// @flow import * as React from 'react'; import { Component, Text, View, Animated, Styles, UserInterface } from 'reactxp'; import { Layout, Container, Header } from './Layout'; import AccountInput from './AccountInput'; import Accordion from './Accordion'; import { formatAccount } from '../lib/formatters'; import Img from './Img'; import { BlueButton, Label, CellButton } from './styled'; import styles from './LoginStyles'; import { colors } from '../config'; import type { LoginState } from '../redux/account/reducers'; import type { AccountToken } from '../lib/daemon-rpc'; export type Props = { accountToken: ?AccountToken, accountHistory: Array, loginError: ?Error, loginState: LoginState, openSettings: ?() => void, openExternalLink: (type: string) => void, login: (accountToken: AccountToken) => void, resetLoginError: () => void, updateAccountToken: (accountToken: AccountToken) => void, removeAccountTokenFromHistory: (accountToken: AccountToken) => Promise, }; type State = { isActive: boolean, }; export default class Login extends Component { state = { isActive: true, }; _accountInput: ?AccountInput; _shouldResetLoginError = false; _showsFooter = true; _footerAnimatedValue = Animated.createValue(0); _footerAnimation: ?Animated.Animation; _footerAnimationStyle: Animated.Style; _footerRef: ?React.Node; _isLoginButtonActive = false; _loginButtonAnimatedValue = Animated.createValue(0); _loginButtonAnimation: ?Animated.Animation; _loginButtonAnimationStyle: Animated.Style; constructor(props: Props) { super(props); if (props.loginState === 'failed') { this._shouldResetLoginError = true; } this._footerAnimationStyle = Styles.createAnimatedViewStyle({ transform: [{ translateY: this._footerAnimatedValue }], }); this._loginButtonAnimationStyle = Styles.createAnimatedViewStyle({ backgroundColor: Animated.interpolate( this._loginButtonAnimatedValue, [0.0, 1.0], [colors.white, colors.green], ), }); } componentDidUpdate(prevProps: Props, _prevState: State) { if ( this.props.loginState !== prevProps.loginState && this.props.loginState === 'failed' && !this._shouldResetLoginError ) { this._shouldResetLoginError = true; // focus on login field when failed to log in const accountInput = this._accountInput; if (accountInput) { accountInput.focus(); } } this._setLoginButtonActive(this._shouldActivateLoginButton()); this._setFooterVisibility(this._shouldShowFooter()); } render() { return (
{this._getStatusIcon()} {this._formTitle()} {this._shouldShowLoginForm() && {this._createLoginForm()}} { this._footerRef = ref; }} style={[styles.login_footer, this._footerAnimationStyle]} testName={'footerVisibility ' + this._shouldShowFooter().toString()}> {this._createFooter()} ); } _onCreateAccount = () => this.props.openExternalLink('createAccount'); _onFocus = () => { this.setState({ isActive: true }); }; _onBlur = (e) => { const relatedTarget = e.relatedTarget; // restore focus if click happened within dropdown if (relatedTarget) { e.target.focus(); return; } this.setState({ isActive: false }); }; async _setLoginButtonActive(isActive: boolean) { if (this._isLoginButtonActive === isActive) { return; } const animation = Animated.timing(this._loginButtonAnimatedValue, { toValue: isActive ? 1 : 0, easing: Animated.Easing.Linear(), duration: 250, }); const oldAnimation = this._loginButtonAnimation; if (oldAnimation) { oldAnimation.stop(); } animation.start(); this._loginButtonAnimation = animation; this._isLoginButtonActive = isActive; } async _setFooterVisibility(show: boolean) { if (this._showsFooter === show) { return; } this._showsFooter = show; const layout = await UserInterface.measureLayoutRelativeToWindow(this._footerRef); const value = show ? 0 : layout.height; const animation = Animated.timing(this._footerAnimatedValue, { toValue: value, easing: Animated.Easing.InOut(), duration: 250, }); const oldAnimation = this._footerAnimation; if (oldAnimation) { oldAnimation.stop(); } animation.start(); this._footerAnimation = animation; } _onLogin = () => { const accountToken = this.props.accountToken; if (accountToken && accountToken.length > 0) { this.props.login(accountToken); } }; _onInputChange = (value: string) => { // reset error when user types in the new account number if (this._shouldResetLoginError) { this._shouldResetLoginError = false; this.props.resetLoginError(); } this.props.updateAccountToken(value); }; _formTitle() { switch (this.props.loginState) { case 'logging in': return 'Logging in...'; case 'failed': return 'Login failed'; case 'ok': return 'Login successful'; default: return 'Login'; } } _formSubtitle() { const { loginState, loginError } = this.props; switch (loginState) { case 'failed': return (loginError && loginError.message) || 'Unknown error'; case 'logging in': return 'Checking account number'; default: return 'Enter your account number'; } } _getStatusIcon() { const statusIconPath = this._getStatusIconPath(); return ( {statusIconPath ? : null} ); } _getStatusIconPath(): ?string { switch (this.props.loginState) { case 'logging in': return 'icon-spinner'; case 'failed': return 'icon-fail'; case 'ok': return 'icon-success'; default: return undefined; } } _accountInputGroupStyles(): Array { const classes = [styles.account_input_group]; if (this.state.isActive) { classes.push(styles.account_input_group__active); } switch (this.props.loginState) { case 'logging in': classes.push(styles.account_input_group__inactive); break; case 'failed': classes.push(styles.account_input_group__error); break; } return classes; } _accountInputButtonStyles(): Array { const classes = [styles.input_button]; if (this.props.loginState === 'logging in') { classes.push(styles.input_button__invisible); } classes.push(this._loginButtonAnimationStyle); return classes; } _accountInputArrowStyles(): Array { const { accountToken, loginState } = this.props; const classes = [styles.input_arrow]; if (accountToken && accountToken.length > 0) { classes.push(styles.input_arrow__active); } if (loginState === 'logging in') { classes.push(styles.input_arrow__invisible); } return classes; } _shouldActivateLoginButton() { const { accountToken } = this.props; return accountToken && accountToken.length > 0; } _shouldEnableAccountInput() { // enable account input always except when "logging in" return this.props.loginState !== 'logging in'; } _shouldShowAccountHistory() { return ( this._shouldEnableAccountInput() && this.state.isActive && this.props.accountHistory.length > 0 ); } _shouldShowLoginForm() { return this.props.loginState !== 'ok'; } _shouldShowFooter() { return ( (this.props.loginState === 'none' || this.props.loginState === 'failed') && !this._shouldShowAccountHistory() ); } _onSelectAccountFromHistory = (accountToken) => { this.props.updateAccountToken(accountToken); this.props.login(accountToken); }; _onRemoveAccountFromHistory = (accountToken) => { this._removeAccountFromHistory(accountToken); }; async _removeAccountFromHistory(accountToken: AccountToken) { try { await this.props.removeAccountTokenFromHistory(accountToken); // TODO: Remove account from memory } catch (error) { // TODO: Show error } } _createLoginForm() { return ( {this._formSubtitle()} (this._accountInput = ref)} testName="AccountInput" /> { } ); } _createFooter() { return ( {"Don't have an account number?"} ); } } type AccountDropdownProps = { items: Array, onSelect: (value: AccountToken) => void, onRemove: (value: AccountToken) => void, }; class AccountDropdown extends React.Component { render() { const uniqueItems = [...new Set(this.props.items)]; return ( {uniqueItems.map((token) => ( ))} ); } } type AccountDropdownItemProps = { label: string, value: AccountToken, onRemove: (value: AccountToken) => void, onSelect: (value: AccountToken) => void, }; class AccountDropdownItem extends React.Component { render() { return ( this.props.onRemove(this.props.value)} /> ); } }