mirror of
https://github.com/elyby/accounts-frontend.git
synced 2025-05-31 14:11:58 +05:30
Change prettier rules
This commit is contained in:
@@ -8,54 +8,50 @@ import MfaDisableForm from './disableForm/MfaDisableForm';
|
||||
import MfaStatus from './status/MfaStatus';
|
||||
|
||||
export default class MfaDisable extends React.Component<
|
||||
{
|
||||
onSubmit: (form: FormModel, sendData: () => Promise<void>) => Promise<void>;
|
||||
onComplete: () => void;
|
||||
},
|
||||
{
|
||||
showForm: boolean;
|
||||
}
|
||||
{
|
||||
onSubmit: (form: FormModel, sendData: () => Promise<void>) => Promise<void>;
|
||||
onComplete: () => void;
|
||||
},
|
||||
{
|
||||
showForm: boolean;
|
||||
}
|
||||
> {
|
||||
static contextType = Context;
|
||||
/* TODO: use declare */ context: React.ContextType<typeof Context>;
|
||||
static contextType = Context;
|
||||
/* TODO: use declare */ context: React.ContextType<typeof Context>;
|
||||
|
||||
state = {
|
||||
showForm: false,
|
||||
};
|
||||
state = {
|
||||
showForm: false,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { showForm } = this.state;
|
||||
render() {
|
||||
const { showForm } = this.state;
|
||||
|
||||
return showForm ? (
|
||||
<MfaDisableForm onSubmit={this.onSubmit} />
|
||||
) : (
|
||||
<MfaStatus onProceed={this.onProceed} />
|
||||
);
|
||||
}
|
||||
return showForm ? <MfaDisableForm onSubmit={this.onSubmit} /> : <MfaStatus onProceed={this.onProceed} />;
|
||||
}
|
||||
|
||||
onProceed = () => this.setState({ showForm: true });
|
||||
onProceed = () => this.setState({ showForm: true });
|
||||
|
||||
onSubmit = (form: FormModel) => {
|
||||
return this.props
|
||||
.onSubmit(form, () => {
|
||||
const { totp, password } = form.serialize() as {
|
||||
totp: string;
|
||||
password?: string;
|
||||
};
|
||||
onSubmit = (form: FormModel) => {
|
||||
return this.props
|
||||
.onSubmit(form, () => {
|
||||
const { totp, password } = form.serialize() as {
|
||||
totp: string;
|
||||
password?: string;
|
||||
};
|
||||
|
||||
return disableMFA(this.context.userId, totp, password);
|
||||
})
|
||||
.then(() => this.props.onComplete())
|
||||
.catch((resp) => {
|
||||
const { errors } = resp || {};
|
||||
return disableMFA(this.context.userId, totp, password);
|
||||
})
|
||||
.then(() => this.props.onComplete())
|
||||
.catch((resp) => {
|
||||
const { errors } = resp || {};
|
||||
|
||||
if (errors) {
|
||||
return Promise.reject(errors);
|
||||
}
|
||||
if (errors) {
|
||||
return Promise.reject(errors);
|
||||
}
|
||||
|
||||
logger.error('MFA: Unexpected disable form result', {
|
||||
resp,
|
||||
});
|
||||
});
|
||||
};
|
||||
logger.error('MFA: Unexpected disable form result', {
|
||||
resp,
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@@ -18,163 +18,156 @@ const STEPS_TOTAL = 3;
|
||||
|
||||
export type MfaStep = 0 | 1 | 2;
|
||||
type Props = {
|
||||
onChangeStep: (nextStep: number) => void;
|
||||
confirmationForm: FormModel;
|
||||
onSubmit: (form: FormModel, sendData: () => Promise<void>) => Promise<void>;
|
||||
onComplete: () => void;
|
||||
step: MfaStep;
|
||||
onChangeStep: (nextStep: number) => void;
|
||||
confirmationForm: FormModel;
|
||||
onSubmit: (form: FormModel, sendData: () => Promise<void>) => Promise<void>;
|
||||
onComplete: () => void;
|
||||
step: MfaStep;
|
||||
};
|
||||
|
||||
interface State {
|
||||
isLoading: boolean;
|
||||
activeStep: MfaStep;
|
||||
secret: string;
|
||||
qrCodeSrc: string;
|
||||
isLoading: boolean;
|
||||
activeStep: MfaStep;
|
||||
secret: string;
|
||||
qrCodeSrc: string;
|
||||
}
|
||||
|
||||
export default class MfaEnable extends React.PureComponent<Props, State> {
|
||||
static contextType = Context;
|
||||
/* TODO: use declare */ context: React.ContextType<typeof Context>;
|
||||
static contextType = Context;
|
||||
/* TODO: use declare */ context: React.ContextType<typeof Context>;
|
||||
|
||||
static defaultProps = {
|
||||
confirmationForm: new FormModel(),
|
||||
step: 0,
|
||||
};
|
||||
static defaultProps = {
|
||||
confirmationForm: new FormModel(),
|
||||
step: 0,
|
||||
};
|
||||
|
||||
state = {
|
||||
isLoading: false,
|
||||
activeStep: this.props.step,
|
||||
qrCodeSrc: '',
|
||||
secret: '',
|
||||
};
|
||||
state = {
|
||||
isLoading: false,
|
||||
activeStep: this.props.step,
|
||||
qrCodeSrc: '',
|
||||
secret: '',
|
||||
};
|
||||
|
||||
confirmationFormEl: Form | null;
|
||||
confirmationFormEl: Form | null;
|
||||
|
||||
componentDidMount() {
|
||||
this.syncState(this.props);
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: Props, state: State) {
|
||||
if (props.step !== state.activeStep) {
|
||||
return {
|
||||
activeStep: props.step,
|
||||
};
|
||||
componentDidMount() {
|
||||
this.syncState(this.props);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.syncState(this.props);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { activeStep, isLoading } = this.state;
|
||||
|
||||
const stepsData = [
|
||||
{
|
||||
buttonLabel: messages.theAppIsInstalled,
|
||||
buttonAction: () => this.nextStep(),
|
||||
},
|
||||
{
|
||||
buttonLabel: messages.ready,
|
||||
buttonAction: () => this.nextStep(),
|
||||
},
|
||||
{
|
||||
buttonLabel: messages.enable,
|
||||
buttonAction: () =>
|
||||
this.confirmationFormEl && this.confirmationFormEl.submit(),
|
||||
},
|
||||
];
|
||||
|
||||
const { buttonLabel, buttonAction } = stepsData[activeStep];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.stepper}>
|
||||
<Stepper totalSteps={STEPS_TOTAL} activeStep={activeStep} />
|
||||
</div>
|
||||
|
||||
<div className={styles.form}>
|
||||
{activeStep > 0 ? <ScrollIntoView /> : null}
|
||||
|
||||
{this.renderStepForms()}
|
||||
|
||||
<Button
|
||||
color="green"
|
||||
onClick={buttonAction}
|
||||
loading={isLoading}
|
||||
block
|
||||
label={buttonLabel}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderStepForms() {
|
||||
const { activeStep, secret, qrCodeSrc } = this.state;
|
||||
|
||||
return (
|
||||
<SlideMotion activeStep={activeStep}>
|
||||
<Instructions key="step1" />
|
||||
<KeyForm key="step2" secret={secret} qrCodeSrc={qrCodeSrc} />
|
||||
<Confirmation
|
||||
key="step3"
|
||||
form={this.props.confirmationForm}
|
||||
formRef={(el) => (this.confirmationFormEl = el)}
|
||||
onSubmit={this.onTotpSubmit}
|
||||
onInvalid={() => this.forceUpdate()}
|
||||
/>
|
||||
</SlideMotion>
|
||||
);
|
||||
}
|
||||
|
||||
syncState(props: Props) {
|
||||
const { isLoading, qrCodeSrc } = this.state;
|
||||
|
||||
if (props.step === 1 && !isLoading && !qrCodeSrc) {
|
||||
this.setState({ isLoading: true });
|
||||
|
||||
getSecret(this.context.userId).then((resp) => {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
secret: resp.secret,
|
||||
qrCodeSrc: resp.qr,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
nextStep() {
|
||||
const nextStep = this.state.activeStep + 1;
|
||||
|
||||
if (nextStep < STEPS_TOTAL) {
|
||||
this.props.onChangeStep(nextStep);
|
||||
}
|
||||
}
|
||||
|
||||
onTotpSubmit = (form: FormModel): Promise<void> => {
|
||||
this.setState({ isLoading: true });
|
||||
|
||||
return this.props
|
||||
.onSubmit(form, () => {
|
||||
const data = form.serialize();
|
||||
|
||||
return enableMFA(this.context.userId, data.totp, data.password);
|
||||
})
|
||||
.then(() => this.props.onComplete())
|
||||
.catch((resp) => {
|
||||
const { errors } = resp || {};
|
||||
|
||||
if (errors) {
|
||||
return Promise.reject(errors);
|
||||
static getDerivedStateFromProps(props: Props, state: State) {
|
||||
if (props.step !== state.activeStep) {
|
||||
return {
|
||||
activeStep: props.step,
|
||||
};
|
||||
}
|
||||
|
||||
logger.error('MFA: Unexpected form submit result', {
|
||||
resp,
|
||||
});
|
||||
})
|
||||
.finally(() => this.setState({ isLoading: false }));
|
||||
};
|
||||
return null;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.syncState(this.props);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { activeStep, isLoading } = this.state;
|
||||
|
||||
const stepsData = [
|
||||
{
|
||||
buttonLabel: messages.theAppIsInstalled,
|
||||
buttonAction: () => this.nextStep(),
|
||||
},
|
||||
{
|
||||
buttonLabel: messages.ready,
|
||||
buttonAction: () => this.nextStep(),
|
||||
},
|
||||
{
|
||||
buttonLabel: messages.enable,
|
||||
buttonAction: () => this.confirmationFormEl && this.confirmationFormEl.submit(),
|
||||
},
|
||||
];
|
||||
|
||||
const { buttonLabel, buttonAction } = stepsData[activeStep];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.stepper}>
|
||||
<Stepper totalSteps={STEPS_TOTAL} activeStep={activeStep} />
|
||||
</div>
|
||||
|
||||
<div className={styles.form}>
|
||||
{activeStep > 0 ? <ScrollIntoView /> : null}
|
||||
|
||||
{this.renderStepForms()}
|
||||
|
||||
<Button color="green" onClick={buttonAction} loading={isLoading} block label={buttonLabel} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderStepForms() {
|
||||
const { activeStep, secret, qrCodeSrc } = this.state;
|
||||
|
||||
return (
|
||||
<SlideMotion activeStep={activeStep}>
|
||||
<Instructions key="step1" />
|
||||
<KeyForm key="step2" secret={secret} qrCodeSrc={qrCodeSrc} />
|
||||
<Confirmation
|
||||
key="step3"
|
||||
form={this.props.confirmationForm}
|
||||
formRef={(el) => (this.confirmationFormEl = el)}
|
||||
onSubmit={this.onTotpSubmit}
|
||||
onInvalid={() => this.forceUpdate()}
|
||||
/>
|
||||
</SlideMotion>
|
||||
);
|
||||
}
|
||||
|
||||
syncState(props: Props) {
|
||||
const { isLoading, qrCodeSrc } = this.state;
|
||||
|
||||
if (props.step === 1 && !isLoading && !qrCodeSrc) {
|
||||
this.setState({ isLoading: true });
|
||||
|
||||
getSecret(this.context.userId).then((resp) => {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
secret: resp.secret,
|
||||
qrCodeSrc: resp.qr,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
nextStep() {
|
||||
const nextStep = this.state.activeStep + 1;
|
||||
|
||||
if (nextStep < STEPS_TOTAL) {
|
||||
this.props.onChangeStep(nextStep);
|
||||
}
|
||||
}
|
||||
|
||||
onTotpSubmit = (form: FormModel): Promise<void> => {
|
||||
this.setState({ isLoading: true });
|
||||
|
||||
return this.props
|
||||
.onSubmit(form, () => {
|
||||
const data = form.serialize();
|
||||
|
||||
return enableMFA(this.context.userId, data.totp, data.password);
|
||||
})
|
||||
.then(() => this.props.onComplete())
|
||||
.catch((resp) => {
|
||||
const { errors } = resp || {};
|
||||
|
||||
if (errors) {
|
||||
return Promise.reject(errors);
|
||||
}
|
||||
|
||||
logger.error('MFA: Unexpected form submit result', {
|
||||
resp,
|
||||
});
|
||||
})
|
||||
.finally(() => this.setState({ isLoading: false }));
|
||||
};
|
||||
}
|
||||
|
@@ -1,25 +1,25 @@
|
||||
{
|
||||
"mfaTitle": "Two‑factor authentication",
|
||||
"mfaDescription": "Two‑factor authentication is an extra layer of security designed to ensure you that you're the only person who can access your account, even if the password gets stolen.",
|
||||
"mfaTitle": "Two‑factor authentication",
|
||||
"mfaDescription": "Two‑factor authentication is an extra layer of security designed to ensure you that you're the only person who can access your account, even if the password gets stolen.",
|
||||
|
||||
"mfaIntroduction": "First of all, you need to install one of our suggested apps on your phone. This app will generate login codes for you. Please choose your OS to get corresponding installation links.",
|
||||
"installOnOfTheApps": "Install one of the following apps:",
|
||||
"findAlternativeApps": "Find alternative apps",
|
||||
"theAppIsInstalled": "App has been installed",
|
||||
"mfaIntroduction": "First of all, you need to install one of our suggested apps on your phone. This app will generate login codes for you. Please choose your OS to get corresponding installation links.",
|
||||
"installOnOfTheApps": "Install one of the following apps:",
|
||||
"findAlternativeApps": "Find alternative apps",
|
||||
"theAppIsInstalled": "App has been installed",
|
||||
|
||||
"scanQrCode": "Open your favorite QR scanner app and scan the following QR code:",
|
||||
"or": "OR",
|
||||
"enterKeyManually": "If you can't scan QR code, try entering your secret key manually:",
|
||||
"whenKeyEntered": "If a temporary code appears in your two‑factor auth app, then you may proceed to the next step.",
|
||||
"ready": "Ready",
|
||||
"scanQrCode": "Open your favorite QR scanner app and scan the following QR code:",
|
||||
"or": "OR",
|
||||
"enterKeyManually": "If you can't scan QR code, try entering your secret key manually:",
|
||||
"whenKeyEntered": "If a temporary code appears in your two‑factor auth app, then you may proceed to the next step.",
|
||||
"ready": "Ready",
|
||||
|
||||
"codePlaceholder": "Enter the code here",
|
||||
"enterCodeFromApp": "In order to finish two‑factor auth setup, please enter the code received in the mobile app:",
|
||||
"enable": "Enable",
|
||||
"codePlaceholder": "Enter the code here",
|
||||
"enterCodeFromApp": "In order to finish two‑factor auth setup, please enter the code received in the mobile app:",
|
||||
"enable": "Enable",
|
||||
|
||||
"disable": "Disable",
|
||||
"mfaEnabledForYourAcc": "Two‑factor authentication for your account is active now",
|
||||
"mfaLoginFlowDesc": "Additional code will be requested next time you log in. Please note, that Minecraft authorization won't work when two‑factor auth is enabled.",
|
||||
"disableMfa": "Disable two‑factor authentication",
|
||||
"disableMfaInstruction": "In order to disable two‑factor authentication, you need to provide a code from your mobile app and confirm your action with your current account password."
|
||||
"disable": "Disable",
|
||||
"mfaEnabledForYourAcc": "Two‑factor authentication for your account is active now",
|
||||
"mfaLoginFlowDesc": "Additional code will be requested next time you log in. Please note, that Minecraft authorization won't work when two‑factor auth is enabled.",
|
||||
"disableMfa": "Disable two‑factor authentication",
|
||||
"disableMfaInstruction": "In order to disable two‑factor authentication, you need to provide a code from your mobile app and confirm your action with your current account password."
|
||||
}
|
||||
|
@@ -10,59 +10,46 @@ import MfaDisable from './MfaDisable';
|
||||
import messages from './MultiFactorAuth.intl.json';
|
||||
|
||||
class MultiFactorAuth extends React.Component<{
|
||||
step: MfaStep;
|
||||
isMfaEnabled: boolean;
|
||||
onSubmit: (form: FormModel, sendData: () => Promise<void>) => Promise<void>;
|
||||
onComplete: () => void;
|
||||
onChangeStep: (nextStep: number) => void;
|
||||
step: MfaStep;
|
||||
isMfaEnabled: boolean;
|
||||
onSubmit: (form: FormModel, sendData: () => Promise<void>) => Promise<void>;
|
||||
onComplete: () => void;
|
||||
onChangeStep: (nextStep: number) => void;
|
||||
}> {
|
||||
render() {
|
||||
const {
|
||||
step,
|
||||
onSubmit,
|
||||
onComplete,
|
||||
onChangeStep,
|
||||
isMfaEnabled,
|
||||
} = this.props;
|
||||
render() {
|
||||
const { step, onSubmit, onComplete, onChangeStep, isMfaEnabled } = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.contentWithBackButton}>
|
||||
<BackButton />
|
||||
return (
|
||||
<div className={styles.contentWithBackButton}>
|
||||
<BackButton />
|
||||
|
||||
<div className={styles.form}>
|
||||
<div className={styles.formBody}>
|
||||
<Message {...messages.mfaTitle}>
|
||||
{(pageTitle) => (
|
||||
<h3 className={styles.title}>
|
||||
<Helmet title={pageTitle as string} />
|
||||
{pageTitle}
|
||||
</h3>
|
||||
)}
|
||||
</Message>
|
||||
<div className={styles.form}>
|
||||
<div className={styles.formBody}>
|
||||
<Message {...messages.mfaTitle}>
|
||||
{(pageTitle) => (
|
||||
<h3 className={styles.title}>
|
||||
<Helmet title={pageTitle as string} />
|
||||
{pageTitle}
|
||||
</h3>
|
||||
)}
|
||||
</Message>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<p className={styles.description}>
|
||||
<Message {...messages.mfaDescription} />
|
||||
</p>
|
||||
<div className={styles.formRow}>
|
||||
<p className={styles.description}>
|
||||
<Message {...messages.mfaDescription} />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isMfaEnabled && <MfaDisable onSubmit={onSubmit} onComplete={onComplete} />}
|
||||
</div>
|
||||
|
||||
{isMfaEnabled || (
|
||||
<MfaEnable step={step} onSubmit={onSubmit} onChangeStep={onChangeStep} onComplete={onComplete} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isMfaEnabled && (
|
||||
<MfaDisable onSubmit={onSubmit} onComplete={onComplete} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isMfaEnabled || (
|
||||
<MfaEnable
|
||||
step={step}
|
||||
onSubmit={onSubmit}
|
||||
onChangeStep={onChangeStep}
|
||||
onComplete={onComplete}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MultiFactorAuth;
|
||||
|
@@ -7,35 +7,35 @@ import profileForm from 'app/components/profile/profileForm.scss';
|
||||
import messages from '../MultiFactorAuth.intl.json';
|
||||
|
||||
export default function Confirmation({
|
||||
form,
|
||||
formRef = () => {},
|
||||
onSubmit,
|
||||
onInvalid,
|
||||
form,
|
||||
formRef = () => {},
|
||||
onSubmit,
|
||||
onInvalid,
|
||||
}: {
|
||||
form: FormModel;
|
||||
formRef?: (el: Form | null) => void;
|
||||
onSubmit: (form: FormModel) => Promise<void> | void;
|
||||
onInvalid: () => void;
|
||||
form: FormModel;
|
||||
formRef?: (el: Form | null) => void;
|
||||
onSubmit: (form: FormModel) => Promise<void> | void;
|
||||
onInvalid: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Form form={form} onSubmit={onSubmit} onInvalid={onInvalid} ref={formRef}>
|
||||
<div className={profileForm.formBody}>
|
||||
<div className={profileForm.formRow}>
|
||||
<p className={profileForm.description}>
|
||||
<Message {...messages.enterCodeFromApp} />
|
||||
</p>
|
||||
</div>
|
||||
return (
|
||||
<Form form={form} onSubmit={onSubmit} onInvalid={onInvalid} ref={formRef}>
|
||||
<div className={profileForm.formBody}>
|
||||
<div className={profileForm.formRow}>
|
||||
<p className={profileForm.description}>
|
||||
<Message {...messages.enterCodeFromApp} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={profileForm.formRow}>
|
||||
<Input
|
||||
{...form.bindField('totp')}
|
||||
required
|
||||
autoComplete="off"
|
||||
skin="light"
|
||||
placeholder={messages.codePlaceholder}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
<div className={profileForm.formRow}>
|
||||
<Input
|
||||
{...form.bindField('totp')}
|
||||
required
|
||||
autoComplete="off"
|
||||
skin="light"
|
||||
placeholder={messages.codePlaceholder}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
@@ -7,43 +7,43 @@ import messages from '../MultiFactorAuth.intl.json';
|
||||
import mfaStyles from '../mfa.scss';
|
||||
|
||||
export default class MfaDisableForm extends React.Component<{
|
||||
onSubmit: (form: FormModel) => Promise<void>;
|
||||
onSubmit: (form: FormModel) => Promise<void>;
|
||||
}> {
|
||||
form: FormModel = new FormModel();
|
||||
form: FormModel = new FormModel();
|
||||
|
||||
render() {
|
||||
const { form } = this;
|
||||
const { onSubmit } = this.props;
|
||||
render() {
|
||||
const { form } = this;
|
||||
const { onSubmit } = this.props;
|
||||
|
||||
return (
|
||||
<Form form={form} onSubmit={onSubmit}>
|
||||
<div className={styles.formBody}>
|
||||
<div className={styles.formRow}>
|
||||
<p className={`${styles.description} ${mfaStyles.mfaTitle}`}>
|
||||
<Message {...messages.disableMfa} />
|
||||
</p>
|
||||
</div>
|
||||
return (
|
||||
<Form form={form} onSubmit={onSubmit}>
|
||||
<div className={styles.formBody}>
|
||||
<div className={styles.formRow}>
|
||||
<p className={`${styles.description} ${mfaStyles.mfaTitle}`}>
|
||||
<Message {...messages.disableMfa} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<p className={styles.description}>
|
||||
<Message {...messages.disableMfaInstruction} />
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.formRow}>
|
||||
<p className={styles.description}>
|
||||
<Message {...messages.disableMfaInstruction} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<Input
|
||||
{...form.bindField('totp')}
|
||||
required
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
skin="light"
|
||||
placeholder={messages.codePlaceholder}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.formRow}>
|
||||
<Input
|
||||
{...form.bindField('totp')}
|
||||
required
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
skin="light"
|
||||
placeholder={messages.codePlaceholder}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button type="submit" color="green" block label={messages.disable} />
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
<Button type="submit" color="green" block label={messages.disable} />
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -12,72 +12,72 @@ import appleLogo from './images/apple.svg';
|
||||
import windowsLogo from './images/windows.svg';
|
||||
|
||||
interface State {
|
||||
activeOs: null | 'android' | 'ios' | 'windows';
|
||||
activeOs: null | 'android' | 'ios' | 'windows';
|
||||
}
|
||||
|
||||
export default class Instructions extends React.Component<{}, State> {
|
||||
state: State = {
|
||||
activeOs: null,
|
||||
};
|
||||
state: State = {
|
||||
activeOs: null,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { activeOs } = this.state;
|
||||
render() {
|
||||
const { activeOs } = this.state;
|
||||
|
||||
return (
|
||||
<div className={profileForm.formBody}>
|
||||
<div className={profileForm.formRow}>
|
||||
<p className={profileForm.description}>
|
||||
<Message {...messages.mfaIntroduction} />
|
||||
</p>
|
||||
</div>
|
||||
return (
|
||||
<div className={profileForm.formBody}>
|
||||
<div className={profileForm.formRow}>
|
||||
<p className={profileForm.description}>
|
||||
<Message {...messages.mfaIntroduction} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={profileForm.formRow}>
|
||||
<div
|
||||
className={clsx(styles.instructionContainer, {
|
||||
[styles.instructionActive]: !!activeOs,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={clsx(styles.osList, {
|
||||
[styles.androidActive]: activeOs === 'android',
|
||||
[styles.appleActive]: activeOs === 'ios',
|
||||
[styles.windowsActive]: activeOs === 'windows',
|
||||
})}
|
||||
>
|
||||
<OsTile
|
||||
className={styles.androidTile}
|
||||
logo={androidLogo}
|
||||
label="Google Play"
|
||||
onClick={(event) => this.onChangeOs(event, 'android')}
|
||||
/>
|
||||
<OsTile
|
||||
className={styles.appleTile}
|
||||
logo={appleLogo}
|
||||
label="App Store"
|
||||
onClick={(event) => this.onChangeOs(event, 'ios')}
|
||||
/>
|
||||
<OsTile
|
||||
className={styles.windowsTile}
|
||||
logo={windowsLogo}
|
||||
label="Windows Store"
|
||||
onClick={(event) => this.onChangeOs(event, 'windows')}
|
||||
/>
|
||||
<div className={profileForm.formRow}>
|
||||
<div
|
||||
className={clsx(styles.instructionContainer, {
|
||||
[styles.instructionActive]: !!activeOs,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={clsx(styles.osList, {
|
||||
[styles.androidActive]: activeOs === 'android',
|
||||
[styles.appleActive]: activeOs === 'ios',
|
||||
[styles.windowsActive]: activeOs === 'windows',
|
||||
})}
|
||||
>
|
||||
<OsTile
|
||||
className={styles.androidTile}
|
||||
logo={androidLogo}
|
||||
label="Google Play"
|
||||
onClick={(event) => this.onChangeOs(event, 'android')}
|
||||
/>
|
||||
<OsTile
|
||||
className={styles.appleTile}
|
||||
logo={appleLogo}
|
||||
label="App Store"
|
||||
onClick={(event) => this.onChangeOs(event, 'ios')}
|
||||
/>
|
||||
<OsTile
|
||||
className={styles.windowsTile}
|
||||
logo={windowsLogo}
|
||||
label="Windows Store"
|
||||
onClick={(event) => this.onChangeOs(event, 'windows')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.osInstructionContainer}>
|
||||
{activeOs ? <OsInstruction os={activeOs} /> : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
<div className={styles.osInstructionContainer}>
|
||||
{activeOs ? <OsInstruction os={activeOs} /> : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
onChangeOs(event: React.MouseEvent, osName: 'android' | 'ios' | 'windows') {
|
||||
event.preventDefault();
|
||||
|
||||
onChangeOs(event: React.MouseEvent, osName: 'android' | 'ios' | 'windows') {
|
||||
event.preventDefault();
|
||||
|
||||
this.setState({
|
||||
activeOs: this.state.activeOs === osName ? null : osName,
|
||||
});
|
||||
}
|
||||
this.setState({
|
||||
activeOs: this.state.activeOs === osName ? null : osName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -7,100 +7,86 @@ import styles from './instructions.scss';
|
||||
type OS = 'android' | 'ios' | 'windows';
|
||||
|
||||
const linksByOs: {
|
||||
[K in OS]: {
|
||||
searchLink: string;
|
||||
featured: Array<{ link: string; label: string }>;
|
||||
};
|
||||
[K in OS]: {
|
||||
searchLink: string;
|
||||
featured: Array<{ link: string; label: string }>;
|
||||
};
|
||||
} = {
|
||||
android: {
|
||||
searchLink: 'https://play.google.com/store/search?q=totp%20authenticator',
|
||||
featured: [
|
||||
{
|
||||
link:
|
||||
'https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2',
|
||||
label: 'Google Authenticator',
|
||||
},
|
||||
{
|
||||
link:
|
||||
'https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp',
|
||||
label: 'FreeOTP Authenticator',
|
||||
},
|
||||
{
|
||||
link:
|
||||
'https://play.google.com/store/apps/details?id=com.authenticator.authservice',
|
||||
label: 'TOTP Authenticator',
|
||||
},
|
||||
],
|
||||
},
|
||||
ios: {
|
||||
searchLink:
|
||||
'https://linkmaker.itunes.apple.com/en-us?mediaType=ios_apps&term=authenticator',
|
||||
featured: [
|
||||
{
|
||||
link:
|
||||
'https://itunes.apple.com/ru/app/google-authenticator/id388497605',
|
||||
label: 'Google Authenticator',
|
||||
},
|
||||
{
|
||||
link:
|
||||
'https://itunes.apple.com/us/app/otp-auth-two-factor-authentication-for-pros/id659877384',
|
||||
label: 'OTP Auth',
|
||||
},
|
||||
{
|
||||
link: 'https://itunes.apple.com/us/app/2stp-authenticator/id954311670',
|
||||
label: '2STP Authenticator',
|
||||
},
|
||||
],
|
||||
},
|
||||
windows: {
|
||||
searchLink:
|
||||
'https://www.microsoft.com/be-by/store/search/apps?devicetype=mobile&q=authenticator',
|
||||
featured: [
|
||||
{
|
||||
link:
|
||||
'https://www.microsoft.com/en-us/store/p/microsoft-authenticator/9nblgggzmcj6',
|
||||
label: 'Microsoft Authenticator',
|
||||
},
|
||||
{
|
||||
link:
|
||||
'https://www.microsoft.com/en-us/store/p/authenticator/9nblggh08h54',
|
||||
label: 'Authenticator+',
|
||||
},
|
||||
{
|
||||
link:
|
||||
'https://www.microsoft.com/en-us/store/p/authenticator-for-windows/9nblggh4n8mx',
|
||||
label: 'Authenticator for Windows',
|
||||
},
|
||||
],
|
||||
},
|
||||
android: {
|
||||
searchLink: 'https://play.google.com/store/search?q=totp%20authenticator',
|
||||
featured: [
|
||||
{
|
||||
link: 'https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2',
|
||||
label: 'Google Authenticator',
|
||||
},
|
||||
{
|
||||
link: 'https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp',
|
||||
label: 'FreeOTP Authenticator',
|
||||
},
|
||||
{
|
||||
link: 'https://play.google.com/store/apps/details?id=com.authenticator.authservice',
|
||||
label: 'TOTP Authenticator',
|
||||
},
|
||||
],
|
||||
},
|
||||
ios: {
|
||||
searchLink: 'https://linkmaker.itunes.apple.com/en-us?mediaType=ios_apps&term=authenticator',
|
||||
featured: [
|
||||
{
|
||||
link: 'https://itunes.apple.com/ru/app/google-authenticator/id388497605',
|
||||
label: 'Google Authenticator',
|
||||
},
|
||||
{
|
||||
link: 'https://itunes.apple.com/us/app/otp-auth-two-factor-authentication-for-pros/id659877384',
|
||||
label: 'OTP Auth',
|
||||
},
|
||||
{
|
||||
link: 'https://itunes.apple.com/us/app/2stp-authenticator/id954311670',
|
||||
label: '2STP Authenticator',
|
||||
},
|
||||
],
|
||||
},
|
||||
windows: {
|
||||
searchLink: 'https://www.microsoft.com/be-by/store/search/apps?devicetype=mobile&q=authenticator',
|
||||
featured: [
|
||||
{
|
||||
link: 'https://www.microsoft.com/en-us/store/p/microsoft-authenticator/9nblgggzmcj6',
|
||||
label: 'Microsoft Authenticator',
|
||||
},
|
||||
{
|
||||
link: 'https://www.microsoft.com/en-us/store/p/authenticator/9nblggh08h54',
|
||||
label: 'Authenticator+',
|
||||
},
|
||||
{
|
||||
link: 'https://www.microsoft.com/en-us/store/p/authenticator-for-windows/9nblggh4n8mx',
|
||||
label: 'Authenticator for Windows',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default function OsInstruction({ os }: { os: OS }) {
|
||||
return (
|
||||
<div
|
||||
className={styles.osInstruction}
|
||||
data-testid="os-instruction"
|
||||
data-os={os}
|
||||
>
|
||||
<h3 className={styles.instructionTitle}>
|
||||
<Message {...messages.installOnOfTheApps} />
|
||||
</h3>
|
||||
return (
|
||||
<div className={styles.osInstruction} data-testid="os-instruction" data-os={os}>
|
||||
<h3 className={styles.instructionTitle}>
|
||||
<Message {...messages.installOnOfTheApps} />
|
||||
</h3>
|
||||
|
||||
<ul className={styles.appList}>
|
||||
{linksByOs[os].featured.map((item) => (
|
||||
<li key={item.label}>
|
||||
<a href={item.link} target="_blank">
|
||||
{item.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<ul className={styles.appList}>
|
||||
{linksByOs[os].featured.map((item) => (
|
||||
<li key={item.label}>
|
||||
<a href={item.link} target="_blank">
|
||||
{item.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className={styles.otherApps}>
|
||||
<a href={linksByOs[os].searchLink} target="_blank">
|
||||
<Message {...messages.findAlternativeApps} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<div className={styles.otherApps}>
|
||||
<a href={linksByOs[os].searchLink} target="_blank">
|
||||
<Message {...messages.findAlternativeApps} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -4,24 +4,20 @@ import clsx from 'clsx';
|
||||
import styles from './instructions.scss';
|
||||
|
||||
export default function OsInstruction({
|
||||
className,
|
||||
logo,
|
||||
label,
|
||||
onClick,
|
||||
className,
|
||||
logo,
|
||||
label,
|
||||
onClick,
|
||||
}: {
|
||||
className: string;
|
||||
logo: string;
|
||||
label: string;
|
||||
onClick: (event: React.MouseEvent<any>) => void;
|
||||
className: string;
|
||||
logo: string;
|
||||
label: string;
|
||||
onClick: (event: React.MouseEvent<any>) => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.osTile, className)}
|
||||
onClick={onClick}
|
||||
data-testid="os-tile"
|
||||
>
|
||||
<img className={styles.osLogo} src={logo} alt={label} />
|
||||
<div className={styles.osName}>{label}</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={clsx(styles.osTile, className)} onClick={onClick} data-testid="os-tile">
|
||||
<img className={styles.osLogo} src={logo} alt={label} />
|
||||
<div className={styles.osName}>{label}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -1,287 +1,287 @@
|
||||
@import '~app/components/ui/fonts.scss';
|
||||
|
||||
.instructionTitle {
|
||||
font-family: $font-family-title;
|
||||
font-size: 14px;
|
||||
font-family: $font-family-title;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.appList {
|
||||
margin: 10px 0;
|
||||
font-size: 11px;
|
||||
margin: 10px 0;
|
||||
font-size: 11px;
|
||||
|
||||
li {
|
||||
margin: 7px 0;
|
||||
}
|
||||
li {
|
||||
margin: 7px 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #666;
|
||||
border-bottom-color: #666;
|
||||
border-bottom-style: dashed;
|
||||
}
|
||||
a {
|
||||
color: #666;
|
||||
border-bottom-color: #666;
|
||||
border-bottom-style: dashed;
|
||||
}
|
||||
}
|
||||
|
||||
.otherApps {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 5px;
|
||||
font-size: 10px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 5px;
|
||||
font-size: 10px;
|
||||
|
||||
a {
|
||||
color: #9e9e9e;
|
||||
border-bottom-color: #9e9e9e;
|
||||
}
|
||||
a {
|
||||
color: #9e9e9e;
|
||||
border-bottom-color: #9e9e9e;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 420px) {
|
||||
$boxHeight: 110px;
|
||||
$boxPadding: 15px;
|
||||
$boxHeight: 110px;
|
||||
$boxPadding: 15px;
|
||||
|
||||
.instructionContainer {
|
||||
position: relative;
|
||||
min-height: $boxHeight + $boxPadding * 2;
|
||||
.instructionContainer {
|
||||
position: relative;
|
||||
min-height: $boxHeight + $boxPadding * 2;
|
||||
|
||||
background: #fff;
|
||||
border: 1px #fff solid;
|
||||
background: #fff;
|
||||
border: 1px #fff solid;
|
||||
|
||||
transition: 0.4s ease;
|
||||
}
|
||||
|
||||
.instructionActive {
|
||||
background: #ebe8e1;
|
||||
border-color: #d8d5ce;
|
||||
}
|
||||
|
||||
.osList {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: $boxPadding;
|
||||
height: $boxHeight;
|
||||
}
|
||||
|
||||
.osTile {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
transition: 0.3s cubic-bezier(0.215, 0.61, 0.355, 1); // easeInOutQuart
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
|
||||
font-family: $font-family-title;
|
||||
}
|
||||
|
||||
.osLogo {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
height: 80px;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.osName {
|
||||
font-size: 15px;
|
||||
margin: 10px 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.androidTile {
|
||||
$translateX: 0;
|
||||
|
||||
transform: translateX($translateX) scale(1);
|
||||
|
||||
&:hover {
|
||||
transform: translateX($translateX) scale(1.1);
|
||||
transition: 0.4s ease;
|
||||
}
|
||||
|
||||
.androidActive & {
|
||||
transform: translateX(0);
|
||||
left: 0;
|
||||
.instructionActive {
|
||||
background: #ebe8e1;
|
||||
border-color: #d8d5ce;
|
||||
}
|
||||
|
||||
.appleActive &,
|
||||
.windowsActive & {
|
||||
transform: translateX($translateX) scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.appleTile {
|
||||
$translateX: 124px;
|
||||
$translateX: -51%;
|
||||
|
||||
transform: translateX($translateX) scale(1);
|
||||
left: 49%;
|
||||
|
||||
&:hover {
|
||||
transform: translateX($translateX) scale(1.1);
|
||||
.osList {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: $boxPadding;
|
||||
height: $boxHeight;
|
||||
}
|
||||
|
||||
.appleActive & {
|
||||
transform: translateX(0);
|
||||
left: 0;
|
||||
.osTile {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
transition: 0.3s cubic-bezier(0.215, 0.61, 0.355, 1); // easeInOutQuart
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
|
||||
font-family: $font-family-title;
|
||||
}
|
||||
|
||||
.androidActive &,
|
||||
.windowsActive & {
|
||||
transform: translateX($translateX) scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.windowsTile {
|
||||
$translateX: 230px;
|
||||
$translateX: -100%;
|
||||
|
||||
transform: translateX($translateX) scale(1);
|
||||
left: 100%;
|
||||
|
||||
&:hover {
|
||||
transform: translateX($translateX) scale(1.1);
|
||||
.osLogo {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
height: 80px;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.windowsActive & {
|
||||
transform: translateX(0);
|
||||
left: 0;
|
||||
.osName {
|
||||
font-size: 15px;
|
||||
margin: 10px 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.appleActive &,
|
||||
.androidActive & {
|
||||
transform: translateX($translateX) scale(0);
|
||||
opacity: 0;
|
||||
.androidTile {
|
||||
$translateX: 0;
|
||||
|
||||
transform: translateX($translateX) scale(1);
|
||||
|
||||
&:hover {
|
||||
transform: translateX($translateX) scale(1.1);
|
||||
}
|
||||
|
||||
.androidActive & {
|
||||
transform: translateX(0);
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.appleActive &,
|
||||
.windowsActive & {
|
||||
transform: translateX($translateX) scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.osInstructionContainer {
|
||||
opacity: 0;
|
||||
transition: 0.4s;
|
||||
.appleTile {
|
||||
$translateX: 124px;
|
||||
$translateX: -51%;
|
||||
|
||||
.instructionActive & {
|
||||
opacity: 1;
|
||||
transform: translateX($translateX) scale(1);
|
||||
left: 49%;
|
||||
|
||||
&:hover {
|
||||
transform: translateX($translateX) scale(1.1);
|
||||
}
|
||||
|
||||
.appleActive & {
|
||||
transform: translateX(0);
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.androidActive &,
|
||||
.windowsActive & {
|
||||
transform: translateX($translateX) scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.osInstruction {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin: 15px;
|
||||
margin-left: 30%;
|
||||
padding-left: 15px;
|
||||
padding-bottom: 15px;
|
||||
min-height: $boxHeight;
|
||||
}
|
||||
.windowsTile {
|
||||
$translateX: 230px;
|
||||
$translateX: -100%;
|
||||
|
||||
transform: translateX($translateX) scale(1);
|
||||
left: 100%;
|
||||
|
||||
&:hover {
|
||||
transform: translateX($translateX) scale(1.1);
|
||||
}
|
||||
|
||||
.windowsActive & {
|
||||
transform: translateX(0);
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.appleActive &,
|
||||
.androidActive & {
|
||||
transform: translateX($translateX) scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.osInstructionContainer {
|
||||
opacity: 0;
|
||||
transition: 0.4s;
|
||||
|
||||
.instructionActive & {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.osInstruction {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin: 15px;
|
||||
margin-left: 30%;
|
||||
padding-left: 15px;
|
||||
padding-bottom: 15px;
|
||||
min-height: $boxHeight;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 420px) {
|
||||
.instructionContainer {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.osList {
|
||||
}
|
||||
|
||||
.osTile {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 48px;
|
||||
|
||||
background: #fff;
|
||||
$borderColor: #eee;
|
||||
border-top: 1px solid $borderColor;
|
||||
border-bottom: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
|
||||
transition: 0.3s cubic-bezier(0.215, 0.61, 0.355, 1); // easeOutCubic
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom-color: $borderColor;
|
||||
.instructionContainer {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.instructionActive & {
|
||||
border-bottom-color: $borderColor;
|
||||
}
|
||||
}
|
||||
|
||||
.osLogo {
|
||||
max-width: 30px;
|
||||
}
|
||||
|
||||
.osName {
|
||||
font-family: $font-family-title;
|
||||
font-size: 16px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@mixin commonNonActiveTile() {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.androidTile {
|
||||
z-index: 3;
|
||||
|
||||
.appleActive & {
|
||||
@include commonNonActiveTile;
|
||||
transform: translateY(-50px);
|
||||
.osList {
|
||||
}
|
||||
|
||||
.windowsActive & {
|
||||
@include commonNonActiveTile;
|
||||
transform: translateY(-100px);
|
||||
}
|
||||
}
|
||||
.osTile {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 48px;
|
||||
|
||||
.appleTile {
|
||||
z-index: 2;
|
||||
background: #fff;
|
||||
$borderColor: #eee;
|
||||
border-top: 1px solid $borderColor;
|
||||
border-bottom: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
|
||||
.appleActive & {
|
||||
transform: translateY(-50px);
|
||||
transition: 0.3s cubic-bezier(0.215, 0.61, 0.355, 1); // easeOutCubic
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom-color: $borderColor;
|
||||
}
|
||||
|
||||
.instructionActive & {
|
||||
border-bottom-color: $borderColor;
|
||||
}
|
||||
}
|
||||
|
||||
.androidActive &,
|
||||
.windowsActive & {
|
||||
@include commonNonActiveTile;
|
||||
transform: translateY(-100px);
|
||||
}
|
||||
}
|
||||
|
||||
.windowsTile {
|
||||
z-index: 1;
|
||||
|
||||
.windowsActive & {
|
||||
transform: translateY(-100px);
|
||||
.osLogo {
|
||||
max-width: 30px;
|
||||
}
|
||||
|
||||
.androidActive &,
|
||||
.appleActive & {
|
||||
@include commonNonActiveTile;
|
||||
transform: translateY(-100px);
|
||||
.osName {
|
||||
font-family: $font-family-title;
|
||||
font-size: 16px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.osInstructionContainer {
|
||||
position: absolute;
|
||||
top: -50px;
|
||||
height: 100px;
|
||||
opacity: 0;
|
||||
transition: 0.4s cubic-bezier(0.23, 1, 0.32, 1); // easeOutQuint
|
||||
width: 100%;
|
||||
box-shadow: inset 0 -1px #eee;
|
||||
|
||||
.instructionActive & {
|
||||
top: 50px;
|
||||
opacity: 1;
|
||||
@mixin commonNonActiveTile() {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.osInstruction {
|
||||
padding-top: 10px;
|
||||
}
|
||||
.androidTile {
|
||||
z-index: 3;
|
||||
|
||||
.otherApps {
|
||||
bottom: 8px;
|
||||
}
|
||||
.appleActive & {
|
||||
@include commonNonActiveTile;
|
||||
transform: translateY(-50px);
|
||||
}
|
||||
|
||||
.windowsActive & {
|
||||
@include commonNonActiveTile;
|
||||
transform: translateY(-100px);
|
||||
}
|
||||
}
|
||||
|
||||
.appleTile {
|
||||
z-index: 2;
|
||||
|
||||
.appleActive & {
|
||||
transform: translateY(-50px);
|
||||
}
|
||||
|
||||
.androidActive &,
|
||||
.windowsActive & {
|
||||
@include commonNonActiveTile;
|
||||
transform: translateY(-100px);
|
||||
}
|
||||
}
|
||||
|
||||
.windowsTile {
|
||||
z-index: 1;
|
||||
|
||||
.windowsActive & {
|
||||
transform: translateY(-100px);
|
||||
}
|
||||
|
||||
.androidActive &,
|
||||
.appleActive & {
|
||||
@include commonNonActiveTile;
|
||||
transform: translateY(-100px);
|
||||
}
|
||||
}
|
||||
|
||||
.osInstructionContainer {
|
||||
position: absolute;
|
||||
top: -50px;
|
||||
height: 100px;
|
||||
opacity: 0;
|
||||
transition: 0.4s cubic-bezier(0.23, 1, 0.32, 1); // easeOutQuint
|
||||
width: 100%;
|
||||
box-shadow: inset 0 -1px #eee;
|
||||
|
||||
.instructionActive & {
|
||||
top: 50px;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.osInstruction {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.otherApps {
|
||||
bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
@@ -7,53 +7,47 @@ import profileForm from 'app/components/profile/profileForm.scss';
|
||||
import messages from '../MultiFactorAuth.intl.json';
|
||||
import styles from './key-form.scss';
|
||||
|
||||
export default function KeyForm({
|
||||
secret,
|
||||
qrCodeSrc,
|
||||
}: {
|
||||
secret: string;
|
||||
qrCodeSrc: string;
|
||||
}) {
|
||||
const formattedSecret = formatSecret(secret || new Array(24).join('X'));
|
||||
export default function KeyForm({ secret, qrCodeSrc }: { secret: string; qrCodeSrc: string }) {
|
||||
const formattedSecret = formatSecret(secret || new Array(24).join('X'));
|
||||
|
||||
return (
|
||||
<div className={profileForm.formBody}>
|
||||
<div className={profileForm.formRow}>
|
||||
<p className={profileForm.description}>
|
||||
<Message {...messages.scanQrCode} />
|
||||
</p>
|
||||
</div>
|
||||
return (
|
||||
<div className={profileForm.formBody}>
|
||||
<div className={profileForm.formRow}>
|
||||
<p className={profileForm.description}>
|
||||
<Message {...messages.scanQrCode} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={profileForm.formRow}>
|
||||
<div className={styles.qrCode}>
|
||||
<ImageLoader ratio={1} src={qrCodeSrc} alt={secret} />
|
||||
<div className={profileForm.formRow}>
|
||||
<div className={styles.qrCode}>
|
||||
<ImageLoader ratio={1} src={qrCodeSrc} alt={secret} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={profileForm.formRow}>
|
||||
<p className={clsx(styles.manualDescription, profileForm.description)}>
|
||||
<span className={styles.or}>
|
||||
<Message {...messages.or} />
|
||||
</span>
|
||||
<Message {...messages.enterKeyManually} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={profileForm.formRow}>
|
||||
<div className={styles.key} data-testid="secret">
|
||||
{formattedSecret}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={profileForm.formRow}>
|
||||
<p className={profileForm.description}>
|
||||
<Message {...messages.whenKeyEntered} />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={profileForm.formRow}>
|
||||
<p className={clsx(styles.manualDescription, profileForm.description)}>
|
||||
<span className={styles.or}>
|
||||
<Message {...messages.or} />
|
||||
</span>
|
||||
<Message {...messages.enterKeyManually} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={profileForm.formRow}>
|
||||
<div className={styles.key} data-testid="secret">
|
||||
{formattedSecret}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={profileForm.formRow}>
|
||||
<p className={profileForm.description}>
|
||||
<Message {...messages.whenKeyEntered} />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
function formatSecret(secret: string): string {
|
||||
return (secret.match(/.{1,4}/g) || []).join(' ');
|
||||
return (secret.match(/.{1,4}/g) || []).join(' ');
|
||||
}
|
||||
|
@@ -1,39 +1,39 @@
|
||||
$maxQrCodeSize: 242px;
|
||||
|
||||
.qrCode {
|
||||
text-align: center;
|
||||
width: $maxQrCodeSize;
|
||||
margin: 0 auto;
|
||||
|
||||
img {
|
||||
text-align: center;
|
||||
width: $maxQrCodeSize;
|
||||
height: $maxQrCodeSize;
|
||||
}
|
||||
margin: 0 auto;
|
||||
|
||||
img {
|
||||
width: $maxQrCodeSize;
|
||||
height: $maxQrCodeSize;
|
||||
}
|
||||
}
|
||||
|
||||
.manualDescription {
|
||||
position: relative;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.or {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
margin-top: -18px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
margin-top: -18px;
|
||||
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
text-transform: capitalize;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.key {
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 359px) {
|
||||
.key {
|
||||
font-size: 13px;
|
||||
}
|
||||
.key {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
@@ -2,37 +2,37 @@
|
||||
@import '~app/components/ui/fonts.scss';
|
||||
|
||||
.mfaTitle {
|
||||
font-size: 18px;
|
||||
font-family: $font-family-title;
|
||||
line-height: 1.2;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
font-family: $font-family-title;
|
||||
line-height: 1.2;
|
||||
text-align: center;
|
||||
|
||||
margin-left: 17%;
|
||||
margin-right: 17%;
|
||||
margin-top: -20px; // TODO: костыль. На странице выключения mfa отступ от текста до заголовка должен быть 20px. На странице информации о статусе от большой иконки до этого заловка должно быть 15px.
|
||||
margin-bottom: -10px; // TODO: костыль. Отступ от заголовка до последующего текста должен быть 20px.
|
||||
margin-left: 17%;
|
||||
margin-right: 17%;
|
||||
margin-top: -20px; // TODO: костыль. На странице выключения mfa отступ от текста до заголовка должен быть 20px. На странице информации о статусе от большой иконки до этого заловка должно быть 15px.
|
||||
margin-bottom: -10px; // TODO: костыль. Отступ от заголовка до последующего текста должен быть 20px.
|
||||
|
||||
@media screen and (max-width: 399px) {
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
@media screen and (max-width: 399px) {
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.bigIcon {
|
||||
color: $blue;
|
||||
font-size: 100px;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
margin-top: -20px; // TODO: костыль. Но отступ большой иконки от текста должен быть 20px.
|
||||
margin-bottom: 35px;
|
||||
color: $blue;
|
||||
font-size: 100px;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
margin-top: -20px; // TODO: костыль. Но отступ большой иконки от текста должен быть 20px.
|
||||
margin-bottom: 35px;
|
||||
}
|
||||
|
||||
.disableMfa {
|
||||
margin-top: -10px; // TODO: костыль. Отступ ссылки от текста должен быть 20px.
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
margin-top: -10px; // TODO: костыль. Отступ ссылки от текста должен быть 20px.
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
|
||||
a {
|
||||
color: #9e9e9e;
|
||||
}
|
||||
a {
|
||||
color: #9e9e9e;
|
||||
}
|
||||
}
|
||||
|
@@ -8,38 +8,38 @@ import messages from '../MultiFactorAuth.intl.json';
|
||||
import mfaStyles from '../mfa.scss';
|
||||
|
||||
export default function MfaStatus({ onProceed }: { onProceed: () => void }) {
|
||||
return (
|
||||
<div className={styles.formBody}>
|
||||
<ScrollIntoView />
|
||||
return (
|
||||
<div className={styles.formBody}>
|
||||
<ScrollIntoView />
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<div className={mfaStyles.bigIcon}>
|
||||
<span className={icons.lock} />
|
||||
<div className={styles.formRow}>
|
||||
<div className={mfaStyles.bigIcon}>
|
||||
<span className={icons.lock} />
|
||||
</div>
|
||||
<p className={`${styles.description} ${mfaStyles.mfaTitle}`}>
|
||||
<Message {...messages.mfaEnabledForYourAcc} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<p className={styles.description}>
|
||||
<Message {...messages.mfaLoginFlowDesc} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={`${styles.formRow} ${mfaStyles.disableMfa}`}>
|
||||
<p className={styles.description}>
|
||||
<a
|
||||
href="#"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
onProceed();
|
||||
}}
|
||||
>
|
||||
<Message {...messages.disable} />
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className={`${styles.description} ${mfaStyles.mfaTitle}`}>
|
||||
<Message {...messages.mfaEnabledForYourAcc} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<p className={styles.description}>
|
||||
<Message {...messages.mfaLoginFlowDesc} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={`${styles.formRow} ${mfaStyles.disableMfa}`}>
|
||||
<p className={styles.description}>
|
||||
<a
|
||||
href="#"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
onProceed();
|
||||
}}
|
||||
>
|
||||
<Message {...messages.disable} />
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user