import React, { useCallback } from 'react'; import { sprintf } from 'sprintf-js'; import { colors } from '../../config.json'; import consumePromise from '../../shared/promise'; 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; removeAccountTokenFromHistory: (accountToken: AccountToken) => 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.shouldShowFooter(); 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 accountTokenValid(): boolean { const { accountToken } = this.props; return accountToken !== undefined && accountToken.length >= MIN_ACCOUNT_TOKEN_LENGTH; } private shouldShowAccountHistory() { return this.allowInteraction() && this.state.isActive && this.props.accountHistory.length > 0; } private shouldShowFooter() { return ( (this.props.loginState.type === 'none' || this.props.loginState.type === 'failed') && !this.shouldShowAccountHistory() ); } private onSelectAccountFromHistory = (accountToken: string) => { this.props.updateAccountToken(accountToken); this.props.login(accountToken); }; private onRemoveAccountFromHistory = (accountToken: string) => { consumePromise(this.removeAccountFromHistory(accountToken)); }; private async removeAccountFromHistory(accountToken: AccountToken) { try { await this.props.removeAccountTokenFromHistory(accountToken); // 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 { items: AccountToken[]; onSelect: (value: AccountToken) => void; onRemove: (value: AccountToken) => void; } function AccountDropdown(props: IAccountDropdownProps) { const uniqueItems = [...new Set(props.items)]; return ( <> {uniqueItems.map((token) => { 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(() => { props.onRemove(props.value); }, [props.onRemove, props.value]); return ( <> {props.label} ); }