Iterate Multi-SSO support
This commit is contained in:
parent
b1ca1eb3f5
commit
f7d7182dc9
7 changed files with 100 additions and 79 deletions
|
@ -37,6 +37,10 @@ limitations under the License.
|
||||||
color: $authpage-primary-color;
|
color: $authpage-primary-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
a:link,
|
a:link,
|
||||||
a:hover,
|
a:hover,
|
||||||
a:visited {
|
a:visited {
|
||||||
|
@ -146,15 +150,14 @@ limitations under the License.
|
||||||
display: block;
|
display: block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 24px;
|
|
||||||
|
|
||||||
> a {
|
> a {
|
||||||
font-weight: $font-semi-bold;
|
font-weight: $font-semi-bold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
form + .mx_AuthBody_changeFlow {
|
.mx_SSOButtons + .mx_AuthBody_changeFlow {
|
||||||
margin-top: 0;
|
margin-top: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AuthBody_spinner {
|
.mx_AuthBody_spinner {
|
||||||
|
|
|
@ -29,8 +29,8 @@ interface ILoginOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Move this to JS SDK
|
// TODO: Move this to JS SDK
|
||||||
interface ILoginFlow {
|
interface IPasswordFlow {
|
||||||
type: "m.login.password" | "m.login.cas";
|
type: "m.login.password";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IIdentityProvider {
|
export interface IIdentityProvider {
|
||||||
|
@ -40,13 +40,13 @@ export interface IIdentityProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISSOFlow {
|
export interface ISSOFlow {
|
||||||
type: "m.login.sso";
|
type: "m.login.sso" | "m.login.cas";
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
identity_providers: IIdentityProvider[];
|
identity_providers: IIdentityProvider[];
|
||||||
"org.matrix.msc2858.identity_providers": IIdentityProvider[]; // Unstable prefix for MSC2858
|
"org.matrix.msc2858.identity_providers": IIdentityProvider[]; // Unstable prefix for MSC2858
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LoginFlow = ISSOFlow | ILoginFlow;
|
export type LoginFlow = ISSOFlow | IPasswordFlow;
|
||||||
|
|
||||||
// TODO: Move this to JS SDK
|
// TODO: Move this to JS SDK
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
|
|
|
@ -2009,6 +2009,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
onLoginClick={this.onLoginClick}
|
onLoginClick={this.onLoginClick}
|
||||||
onServerConfigChange={this.onServerConfigChange}
|
onServerConfigChange={this.onServerConfigChange}
|
||||||
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
|
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
|
||||||
|
fragmentAfterLogin={fragmentAfterLogin}
|
||||||
{...this.getServerProperties()}
|
{...this.getServerProperties()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -319,6 +319,7 @@ export default class ForgotPassword extends React.Component {
|
||||||
onChange={this.onInputChanged.bind(this, "password")}
|
onChange={this.onInputChanged.bind(this, "password")}
|
||||||
onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_focus")}
|
onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_focus")}
|
||||||
onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_blur")}
|
onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_blur")}
|
||||||
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
<Field
|
<Field
|
||||||
name="reset_password_confirm"
|
name="reset_password_confirm"
|
||||||
|
@ -328,6 +329,7 @@ export default class ForgotPassword extends React.Component {
|
||||||
onChange={this.onInputChanged.bind(this, "password2")}
|
onChange={this.onInputChanged.bind(this, "password2")}
|
||||||
onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword2_focus")}
|
onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword2_focus")}
|
||||||
onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword2_blur")}
|
onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword2_blur")}
|
||||||
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span>{_t(
|
<span>{_t(
|
||||||
|
|
|
@ -438,7 +438,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
||||||
|
|
||||||
if (supportedFlows.length > 0) {
|
if (supportedFlows.length > 0) {
|
||||||
this.setState({
|
this.setState({
|
||||||
currentFlow: this.getCurrentFlowStep(),
|
flows: supportedFlows,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -520,22 +520,13 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (PHASES_ENABLED && this.state.phase !== Phase.ServerDetails) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const serverDetailsProps: ComponentProps<typeof ServerConfig> = {};
|
|
||||||
if (PHASES_ENABLED) {
|
|
||||||
serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick;
|
|
||||||
serverDetailsProps.submitText = _t("Next");
|
|
||||||
serverDetailsProps.submitClass = "mx_Login_submit";
|
|
||||||
}
|
|
||||||
|
|
||||||
return <ServerConfig
|
return <ServerConfig
|
||||||
serverConfig={this.props.serverConfig}
|
serverConfig={this.props.serverConfig}
|
||||||
onServerConfigChange={this.props.onServerConfigChange}
|
onServerConfigChange={this.props.onServerConfigChange}
|
||||||
delayTimeMs={250}
|
delayTimeMs={250}
|
||||||
{...serverDetailsProps}
|
onAfterSubmit={this.onServerDetailsNextPhaseClick}
|
||||||
|
submitText={_t("Next")}
|
||||||
|
submitClass="mx_Login_submit"
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -591,7 +582,6 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
||||||
const flow = this.state.flows.find(flow => flow.type === "m.login." + loginType) as ISSOFlow;
|
const flow = this.state.flows.find(flow => flow.type === "m.login." + loginType) as ISSOFlow;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
|
||||||
<SSOButtons
|
<SSOButtons
|
||||||
matrixClient={this.loginLogic.createTemporaryClient()}
|
matrixClient={this.loginLogic.createTemporaryClient()}
|
||||||
flow={flow}
|
flow={flow}
|
||||||
|
@ -599,7 +589,6 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
||||||
fragmentAfterLogin={this.props.fragmentAfterLogin}
|
fragmentAfterLogin={this.props.fragmentAfterLogin}
|
||||||
primary={!this.state.flows.find(flow => flow.type === "m.login.password")}
|
primary={!this.state.flows.find(flow => flow.type === "m.login.password")}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Matrix from 'matrix-js-sdk';
|
import Matrix from 'matrix-js-sdk';
|
||||||
import React, {ComponentProps, ReactNode} from 'react';
|
import React, {ReactNode} from 'react';
|
||||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||||
|
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
|
@ -28,8 +28,9 @@ import classNames from "classnames";
|
||||||
import * as Lifecycle from '../../../Lifecycle';
|
import * as Lifecycle from '../../../Lifecycle';
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||||
import AuthPage from "../../views/auth/AuthPage";
|
import AuthPage from "../../views/auth/AuthPage";
|
||||||
import Login from "../../../Login";
|
import Login, {ISSOFlow} from "../../../Login";
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
|
import SSOButtons from "../../views/elements/SSOButtons";
|
||||||
|
|
||||||
// Phases
|
// Phases
|
||||||
enum Phase {
|
enum Phase {
|
||||||
|
@ -47,6 +48,7 @@ interface IProps {
|
||||||
clientSecret?: string;
|
clientSecret?: string;
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
idSid?: string;
|
idSid?: string;
|
||||||
|
fragmentAfterLogin?: string;
|
||||||
|
|
||||||
// Called when the user has logged in. Params:
|
// Called when the user has logged in. Params:
|
||||||
// - object with userId, deviceId, homeserverUrl, identityServerUrl, accessToken
|
// - object with userId, deviceId, homeserverUrl, identityServerUrl, accessToken
|
||||||
|
@ -116,12 +118,14 @@ interface IState {
|
||||||
// if a different user ID to the one we just registered is logged in,
|
// if a different user ID to the one we just registered is logged in,
|
||||||
// this is the user ID that's logged in.
|
// this is the user ID that's logged in.
|
||||||
differentLoggedInUserId?: string;
|
differentLoggedInUserId?: string;
|
||||||
|
// the SSO flow definition, this is fetched from /login as that's the only
|
||||||
|
// place it is exposed.
|
||||||
|
ssoFlow?: ISSOFlow;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable phases for registration
|
|
||||||
const PHASES_ENABLED = true;
|
|
||||||
|
|
||||||
export default class Registration extends React.Component<IProps, IState> {
|
export default class Registration extends React.Component<IProps, IState> {
|
||||||
|
loginLogic: Login;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
@ -141,6 +145,11 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||||
serverErrorIsFatal: false,
|
serverErrorIsFatal: false,
|
||||||
serverDeadError: "",
|
serverDeadError: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const {hsUrl, isUrl} = this.props.serverConfig;
|
||||||
|
this.loginLogic = new Login(hsUrl, isUrl, null, {
|
||||||
|
defaultDeviceDisplayName: "Element login check", // We shouldn't ever be used
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -252,9 +261,21 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||||
console.log("Unable to determine is server needs id_server param", e);
|
console.log("Unable to determine is server needs id_server param", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.loginLogic.setHomeserverUrl(hsUrl);
|
||||||
|
this.loginLogic.setIdentityServerUrl(isUrl);
|
||||||
|
|
||||||
|
let ssoFlow: ISSOFlow;
|
||||||
|
try {
|
||||||
|
const loginFlows = await this.loginLogic.getFlows();
|
||||||
|
ssoFlow = loginFlows.find(f => f.type === "m.login.sso" || f.type === "m.login.cas") as ISSOFlow;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to get login flows to check for SSO support", e);
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
matrixClient: cli,
|
matrixClient: cli,
|
||||||
serverRequiresIdServer,
|
serverRequiresIdServer,
|
||||||
|
ssoFlow,
|
||||||
busy: false,
|
busy: false,
|
||||||
});
|
});
|
||||||
const showGenericError = (e) => {
|
const showGenericError = (e) => {
|
||||||
|
@ -282,13 +303,7 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||||
// At this point registration is pretty much disabled, but before we do that let's
|
// At this point registration is pretty much disabled, but before we do that let's
|
||||||
// quickly check to see if the server supports SSO instead. If it does, we'll send
|
// quickly check to see if the server supports SSO instead. If it does, we'll send
|
||||||
// the user off to the login page to figure their account out.
|
// the user off to the login page to figure their account out.
|
||||||
try {
|
if (ssoFlow) {
|
||||||
const loginLogic = new Login(hsUrl, isUrl, null, {
|
|
||||||
defaultDeviceDisplayName: "Element login check", // We shouldn't ever be used
|
|
||||||
});
|
|
||||||
const flows = await loginLogic.getFlows();
|
|
||||||
const hasSsoFlow = flows.find(f => f.type === 'm.login.sso' || f.type === 'm.login.cas');
|
|
||||||
if (hasSsoFlow) {
|
|
||||||
// Redirect to login page - server probably expects SSO only
|
// Redirect to login page - server probably expects SSO only
|
||||||
dis.dispatch({action: 'start_login'});
|
dis.dispatch({action: 'start_login'});
|
||||||
} else {
|
} else {
|
||||||
|
@ -299,10 +314,6 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||||
flows: [],
|
flows: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to get login flows to check for SSO support", e);
|
|
||||||
showGenericError(e);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.log("Unable to query for supported registration methods.", e);
|
console.log("Unable to query for supported registration methods.", e);
|
||||||
showGenericError(e);
|
showGenericError(e);
|
||||||
|
@ -534,7 +545,7 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||||
// which is always shown if we allow custom URLs at all.
|
// which is always shown if we allow custom URLs at all.
|
||||||
// (if there's a fatal server error, we need to show the full server
|
// (if there's a fatal server error, we need to show the full server
|
||||||
// config as the user may need to change servers to resolve the error).
|
// config as the user may need to change servers to resolve the error).
|
||||||
if (PHASES_ENABLED && this.state.phase !== Phase.ServerDetails && !this.state.serverErrorIsFatal) {
|
if (this.state.phase !== Phase.ServerDetails && !this.state.serverErrorIsFatal) {
|
||||||
return <div>
|
return <div>
|
||||||
<ServerTypeSelector
|
<ServerTypeSelector
|
||||||
selected={this.state.serverType}
|
selected={this.state.serverType}
|
||||||
|
@ -543,13 +554,6 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverDetailsProps: ComponentProps<typeof ServerConfig> = {};
|
|
||||||
if (PHASES_ENABLED) {
|
|
||||||
serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick;
|
|
||||||
serverDetailsProps.submitText = _t("Next");
|
|
||||||
serverDetailsProps.submitClass = "mx_Login_submit";
|
|
||||||
}
|
|
||||||
|
|
||||||
let serverDetails = null;
|
let serverDetails = null;
|
||||||
switch (this.state.serverType) {
|
switch (this.state.serverType) {
|
||||||
case ServerType.FREE:
|
case ServerType.FREE:
|
||||||
|
@ -559,7 +563,9 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||||
serverConfig={this.props.serverConfig}
|
serverConfig={this.props.serverConfig}
|
||||||
onServerConfigChange={this.props.onServerConfigChange}
|
onServerConfigChange={this.props.onServerConfigChange}
|
||||||
delayTimeMs={250}
|
delayTimeMs={250}
|
||||||
{...serverDetailsProps}
|
onAfterSubmit={this.onServerDetailsNextPhaseClick}
|
||||||
|
submitText={_t("Next")}
|
||||||
|
submitClass="mx_Login_submit"
|
||||||
/>;
|
/>;
|
||||||
break;
|
break;
|
||||||
case ServerType.ADVANCED:
|
case ServerType.ADVANCED:
|
||||||
|
@ -568,7 +574,9 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||||
onServerConfigChange={this.props.onServerConfigChange}
|
onServerConfigChange={this.props.onServerConfigChange}
|
||||||
delayTimeMs={250}
|
delayTimeMs={250}
|
||||||
showIdentityServerIfRequiredByHomeserver={true}
|
showIdentityServerIfRequiredByHomeserver={true}
|
||||||
{...serverDetailsProps}
|
onAfterSubmit={this.onServerDetailsNextPhaseClick}
|
||||||
|
submitText={_t("Next")}
|
||||||
|
submitClass="mx_Login_submit"
|
||||||
/>;
|
/>;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -583,7 +591,7 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderRegisterComponent() {
|
private renderRegisterComponent() {
|
||||||
if (PHASES_ENABLED && this.state.phase !== Phase.Registration) {
|
if (this.state.phase !== Phase.Registration) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -610,7 +618,23 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>;
|
</div>;
|
||||||
} else if (this.state.flows.length) {
|
} else if (this.state.flows.length) {
|
||||||
return <RegistrationForm
|
let ssoSection;
|
||||||
|
if (this.state.ssoFlow) {
|
||||||
|
ssoSection = <React.Fragment>
|
||||||
|
<h4>{_t("Continue with")}</h4>
|
||||||
|
<SSOButtons
|
||||||
|
matrixClient={this.loginLogic.createTemporaryClient()}
|
||||||
|
flow={this.state.ssoFlow}
|
||||||
|
loginType={this.state.ssoFlow.type === "m.login.sso" ? "sso" : "cas"}
|
||||||
|
fragmentAfterLogin={this.props.fragmentAfterLogin}
|
||||||
|
/>
|
||||||
|
<h4>{_t("Or")}</h4>
|
||||||
|
</React.Fragment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <React.Fragment>
|
||||||
|
{ ssoSection }
|
||||||
|
<RegistrationForm
|
||||||
defaultUsername={this.state.formVals.username}
|
defaultUsername={this.state.formVals.username}
|
||||||
defaultEmail={this.state.formVals.email}
|
defaultEmail={this.state.formVals.email}
|
||||||
defaultPhoneCountry={this.state.formVals.phoneCountry}
|
defaultPhoneCountry={this.state.formVals.phoneCountry}
|
||||||
|
@ -621,7 +645,8 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||||
serverConfig={this.props.serverConfig}
|
serverConfig={this.props.serverConfig}
|
||||||
canSubmit={!this.state.serverErrorIsFatal}
|
canSubmit={!this.state.serverErrorIsFatal}
|
||||||
serverRequiresIdServer={this.state.serverRequiresIdServer}
|
serverRequiresIdServer={this.state.serverRequiresIdServer}
|
||||||
/>;
|
/>
|
||||||
|
</React.Fragment>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -658,7 +683,7 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
// Only show the 'go back' button if you're not looking at the form
|
// Only show the 'go back' button if you're not looking at the form
|
||||||
let goBack;
|
let goBack;
|
||||||
if ((PHASES_ENABLED && this.state.phase !== Phase.Registration) || this.state.doingUIAuth) {
|
if (this.state.phase !== Phase.Registration || this.state.doingUIAuth) {
|
||||||
goBack = <a className="mx_AuthBody_changeFlow" onClick={this.onGoToFormClicked} href="#">
|
goBack = <a className="mx_AuthBody_changeFlow" onClick={this.onGoToFormClicked} href="#">
|
||||||
{ _t('Go back') }
|
{ _t('Go back') }
|
||||||
</a>;
|
</a>;
|
||||||
|
@ -725,8 +750,7 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||||
// If custom URLs are allowed, user is not doing UIA flows and they haven't selected the Free server type,
|
// If custom URLs are allowed, user is not doing UIA flows and they haven't selected the Free server type,
|
||||||
// wire up the server details edit link.
|
// wire up the server details edit link.
|
||||||
let editLink = null;
|
let editLink = null;
|
||||||
if (PHASES_ENABLED &&
|
if (!SdkConfig.get()['disable_custom_urls'] &&
|
||||||
!SdkConfig.get()['disable_custom_urls'] &&
|
|
||||||
this.state.serverType !== ServerType.FREE &&
|
this.state.serverType !== ServerType.FREE &&
|
||||||
!this.state.doingUIAuth
|
!this.state.doingUIAuth
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -2517,6 +2517,8 @@
|
||||||
"Unable to query for supported registration methods.": "Unable to query for supported registration methods.",
|
"Unable to query for supported registration methods.": "Unable to query for supported registration methods.",
|
||||||
"Registration has been disabled on this homeserver.": "Registration has been disabled on this homeserver.",
|
"Registration has been disabled on this homeserver.": "Registration has been disabled on this homeserver.",
|
||||||
"This server does not support authentication with a phone number.": "This server does not support authentication with a phone number.",
|
"This server does not support authentication with a phone number.": "This server does not support authentication with a phone number.",
|
||||||
|
"Continue with": "Continue with",
|
||||||
|
"Or": "Or",
|
||||||
"Already have an account? <a>Sign in here</a>": "Already have an account? <a>Sign in here</a>",
|
"Already have an account? <a>Sign in here</a>": "Already have an account? <a>Sign in here</a>",
|
||||||
"Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).",
|
"Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).",
|
||||||
"Continue with previous account": "Continue with previous account",
|
"Continue with previous account": "Continue with previous account",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue