Change prettier rules

This commit is contained in:
ErickSkrauch
2020-05-24 02:08:24 +03:00
parent 73f0c37a6a
commit f85b9d8d35
382 changed files with 24137 additions and 26046 deletions

View File

@@ -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,
});
});
};
}

View File

@@ -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 }));
};
}

View File

@@ -1,25 +1,25 @@
{
"mfaTitle": "Twofactor authentication",
"mfaDescription": "Twofactor 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": "Twofactor authentication",
"mfaDescription": "Twofactor 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 twofactor 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 twofactor auth app, then you may proceed to the next step.",
"ready": "Ready",
"codePlaceholder": "Enter the code here",
"enterCodeFromApp": "In order to finish twofactor auth setup, please enter the code received in the mobile app:",
"enable": "Enable",
"codePlaceholder": "Enter the code here",
"enterCodeFromApp": "In order to finish twofactor auth setup, please enter the code received in the mobile app:",
"enable": "Enable",
"disable": "Disable",
"mfaEnabledForYourAcc": "Twofactor 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 twofactor auth is enabled.",
"disableMfa": "Disable twofactor authentication",
"disableMfaInstruction": "In order to disable twofactor authentication, you need to provide a code from your mobile app and confirm your action with your current account password."
"disable": "Disable",
"mfaEnabledForYourAcc": "Twofactor 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 twofactor auth is enabled.",
"disableMfa": "Disable twofactor authentication",
"disableMfaInstruction": "In order to disable twofactor authentication, you need to provide a code from your mobile app and confirm your action with your current account password."
}

View File

@@ -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;

View File

@@ -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>
);
}

View File

@@ -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>
);
}
}

View File

@@ -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,
});
}
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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;
}
}

View File

@@ -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(' ');
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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>
);
);
}