import React, { useCallback } from 'react'; import { sprintf } from 'sprintf-js'; import { colors } from '../../config.json'; import { messages } from '../../shared/gettext'; import { formatAccountToken } from '../lib/account'; import Accordion from './Accordion'; import * as AppButton from './AppButton'; import { Brand, HeaderBarSettingsButton } from './HeaderBar'; import ImageView from './ImageView'; import { Container, Header, Layout } from './Layout'; import { StyledAccountDropdownContainer, StyledAccountDropdownItem, StyledAccountDropdownItemButton, StyledAccountDropdownItemButtonLabel, StyledAccountDropdownRemoveButton, StyledAccountDropdownRemoveIcon, StyledAccountInputBackdrop, StyledAccountInputGroup, StyledDropdownSpacer, StyledFooter, StyledInput, StyledInputButton, StyledInputSubmitIcon, StyledLoginFooterPrompt, StyledLoginForm, StyledStatusIcon, StyledSubtitle, StyledTitle, } from './LoginStyles'; import { AccountToken } from '../../shared/daemon-rpc-types'; import { LoginState } from '../redux/account/reducers'; import { AriaControlGroup, AriaControlled, AriaControls } from './AriaGroup'; interface IProps { accountToken?: AccountToken; accountHistory?: AccountToken; loginState: LoginState; openExternalLink: (type: string) => void; login: (accountToken: AccountToken) => void; resetLoginError: () => void; updateAccountToken: (accountToken: AccountToken) => void; clearAccountHistory: () => Promise; createNewAccount: () => void; } interface IState { isActive: boolean; } const MIN_ACCOUNT_TOKEN_LENGTH = 10; export default class Login extends React.Component { public state: IState = { isActive: true, }; private accountInput = React.createRef(); private shouldResetLoginError = false; constructor(props: IProps) { super(props); if (props.loginState.type === 'failed') { this.shouldResetLoginError = true; } } public componentDidUpdate(prevProps: IProps, _prevState: IState) { if ( this.props.loginState.type !== prevProps.loginState.type && this.props.loginState.type === 'failed' && !this.shouldResetLoginError ) { this.shouldResetLoginError = true; // focus on login field when failed to log in this.accountInput.current?.focus(); } } public render() { const showFooter = this.allowInteraction(); return (
{this.getStatusIcon()} {this.formTitle()} {this.createLoginForm()} {this.createFooter()}
); } private onFocus = () => { this.setState({ isActive: true }); }; private onBlur = (e: React.FocusEvent) => { // restore focus if click happened within dropdown if (e.relatedTarget) { if (this.accountInput.current) { this.accountInput.current.focus(); } return; } this.setState({ isActive: false }); }; private onSubmit = (event?: React.FormEvent) => { event?.preventDefault(); if (this.accountTokenValid()) { this.props.login(this.props.accountToken!); } }; private onInputChange = (accountToken: string) => { // reset error when user types in the new account number if (this.shouldResetLoginError) { this.shouldResetLoginError = false; this.props.resetLoginError(); } this.props.updateAccountToken(accountToken); }; private formTitle() { switch (this.props.loginState.type) { case 'logging in': return this.props.loginState.method === 'existing_account' ? messages.pgettext('login-view', 'Logging in...') : messages.pgettext('login-view', 'Creating account...'); case 'failed': return this.props.loginState.method === 'existing_account' ? messages.pgettext('login-view', 'Login failed') : messages.pgettext('login-view', 'Error'); case 'ok': return this.props.loginState.method === 'existing_account' ? messages.pgettext('login-view', 'Logged in') : messages.pgettext('login-view', 'Account created'); default: return messages.pgettext('login-view', 'Login'); } } private formSubtitle() { switch (this.props.loginState.type) { case 'failed': return this.props.loginState.method === 'existing_account' ? this.props.loginState.error.message || messages.pgettext('login-view', 'Unknown error') : messages.pgettext('login-view', 'Failed to create account'); case 'logging in': return this.props.loginState.method === 'existing_account' ? messages.pgettext('login-view', 'Checking account number') : messages.pgettext('login-view', 'Please wait'); case 'ok': return this.props.loginState.method === 'existing_account' ? messages.pgettext('login-view', 'Valid account number') : messages.pgettext('login-view', 'Logged in'); default: return messages.pgettext('login-view', 'Enter your account number'); } } private getStatusIcon() { const statusIconPath = this.getStatusIconPath(); return ( {statusIconPath ? : null} ); } private getStatusIconPath(): string | undefined { switch (this.props.loginState.type) { case 'logging in': return 'icon-spinner'; case 'failed': return 'icon-fail'; case 'ok': return 'icon-success'; default: return undefined; } } private allowInteraction() { return this.props.loginState.type !== 'logging in' && this.props.loginState.type !== 'ok'; } private allowCreateAccount() { const { accountToken } = this.props; return this.allowInteraction() && (accountToken === undefined || accountToken.length === 0); } private accountTokenValid(): boolean { const { accountToken } = this.props; return accountToken !== undefined && accountToken.length >= MIN_ACCOUNT_TOKEN_LENGTH; } private shouldShowAccountHistory() { return this.allowInteraction() && this.props.accountHistory !== undefined; } private onSelectAccountFromHistory = (accountToken: string) => { this.props.updateAccountToken(accountToken); this.props.login(accountToken); }; private onClearAccountHistory = () => { void this.clearAccountHistory(); }; private async clearAccountHistory() { try { await this.props.clearAccountHistory(); // TODO: Remove account from memory } catch (error) { // TODO: Show error } } private createLoginForm() { const allowInteraction = this.allowInteraction(); const allowLogin = allowInteraction && this.accountTokenValid(); const hasError = this.props.loginState.type === 'failed' && this.props.loginState.method === 'existing_account'; return ( <> {this.formSubtitle()} ); } private createFooter() { return ( <> {messages.pgettext('login-view', "Don't have an account number?")} {messages.pgettext('login-view', 'Create account')} ); } } interface IAccountDropdownProps { item?: AccountToken; onSelect: (value: AccountToken) => void; onRemove: (value: AccountToken) => void; } function AccountDropdown(props: IAccountDropdownProps) { const token = props.item; if (!token) { return null; } const label = formatAccountToken(token); return ( ); } interface IAccountDropdownItemProps { label: string; value: AccountToken; onRemove: (value: AccountToken) => void; onSelect: (value: AccountToken) => void; } function AccountDropdownItem(props: IAccountDropdownItemProps) { const handleSelect = useCallback(() => { props.onSelect(props.value); }, [props.onSelect, props.value]); const handleRemove = useCallback( (event) => { // Prevent login form from submitting event.preventDefault(); props.onRemove(props.value); }, [props.onRemove, props.value], ); return ( <> {props.label} ); }