// @flow import * as React from 'react'; import { Component, Text, View, Animated, Styles } 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 { AccountReduxState } from '../redux/account/reducers'; import type { AccountToken } from '../lib/ipc-facade'; export type LoginPropTypes = { account: AccountReduxState, onLogin: (accountToken: AccountToken) => void, onSettings: ?() => void, onFirstChangeAfterFailure: () => void, onExternalLink: (type: string) => void, onAccountTokenChange: (accountToken: AccountToken) => void, onRemoveAccountTokenFromHistory: (accountToken: AccountToken) => void, }; type State = { notifyOnFirstChangeAfterFailure: boolean, isActive: boolean, footerHeight: number, animatedFooterValue: Animated.Value, animatedLoginButtonValue: Animated.Value, animation: ?Animated.CompositeAnimation, footerAnimationStyle: ?Animated.Style, loginButtonAnimationStyle: ?Animated.Style, }; export default class Login extends Component { state = { notifyOnFirstChangeAfterFailure: false, isActive: true, footerHeight: 0, animatedFooterValue: Animated.createValue(0), animatedLoginButtonValue: Animated.createValue(0), animation: null, footerAnimationStyle: {}, loginButtonAnimationStyle: {}, }; constructor(props: LoginPropTypes) { super(props); if (props.account.status === 'failed') { this.state.notifyOnFirstChangeAfterFailure = true; } this.state.footerAnimationStyle = Styles.createAnimatedViewStyle({ transform: [{ translateY: this.state.animatedFooterValue }], }); this.state.loginButtonAnimationStyle = Styles.createAnimatedViewStyle({ backgroundColor: Animated.interpolate( this.state.animatedLoginButtonValue, [0.0, 1.0], [colors.white, colors.green], ), }); } componentWillReceiveProps(nextProps: LoginPropTypes) { const prev = this.props.account || {}; const next = nextProps.account || {}; if (prev.status !== next.status && next.status === 'failed') { this.setState({ notifyOnFirstChangeAfterFailure: true }); } this._animate(nextProps); } render() { return (
{this._getStatusIcon()} {this._formTitle()} {this._shouldShowLoginForm() && {this._createLoginForm()}} {this._createFooter()} ); } _onCreateAccount = () => this.props.onExternalLink('createAccount'); _onFocus = () => this.setState({ isActive: true }, () => { this._animate(this.props); }); _onBlur = (e) => { const relatedTarget = e.relatedTarget; // restore focus if click happened within dropdown if (relatedTarget) { e.target.focus(); return; } this.setState({ isActive: false }, () => { this._animate(this.props); }); }; _animate = (props: LoginPropTypes) => { if (this.state.animation) { this.state.animation.stop(); } const accountToken = props.account.accountToken || []; const footerPosition = this._shouldShowFooter(props) ? 0 : this.state.footerHeight; const loginButtonValue = accountToken.length > 0 ? 1 : 0; this._setAnimation( this._getFooterAnimation(footerPosition), this._getLoginButtonAnimation(loginButtonValue), ); }; _setAnimation = ( footerAnimation: Animated.CompositeAnimation, loginButtonAnimation: Animated.CompositeAnimation, ) => { let compositeAnimation = Animated.parallel([footerAnimation, loginButtonAnimation]); this.setState({ animation: compositeAnimation }, () => { compositeAnimation.start(() => this.setState({ animation: null, }), ); }); }; _onLogin = () => { const accountToken = this.props.account.accountToken; if (accountToken && accountToken.length > 0) { this.props.onLogin(accountToken); } }; _onInputChange = (value: string) => { // notify delegate on first change after login failure if (this.state.notifyOnFirstChangeAfterFailure) { this.setState({ notifyOnFirstChangeAfterFailure: false }); this.props.onFirstChangeAfterFailure(); } this.props.onAccountTokenChange(value); }; _formTitle() { switch (this.props.account.status) { case 'logging in': return 'Logging in...'; case 'failed': return 'Login failed'; case 'ok': return 'Login successful'; default: return 'Login'; } } _formSubtitle() { const { status, error } = this.props.account; switch (status) { case 'failed': return (error && error.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.account.status) { 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.account.status) { 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 { status } = this.props.account; const classes = [styles.input_button]; if (status === 'logging in') { classes.push(styles.input_button__invisible); } classes.push(this.state.loginButtonAnimationStyle); return classes; } _accountInputArrowStyles(): Array { const { accountToken, status } = this.props.account; const classes = [styles.input_arrow]; if (accountToken && accountToken.length > 0) { classes.push(styles.input_arrow__active); } if (status === 'logging in') { classes.push(styles.input_arrow__invisible); } return classes; } _shouldEnableAccountInput(props: LoginPropTypes) { // enable account input always except when "logging in" return props.account.status !== 'logging in'; } _shouldShowAccountHistory(props: LoginPropTypes) { return ( this._shouldEnableAccountInput(props) && this.state.isActive && props.account.accountHistory.length > 0 ); } _shouldShowLoginForm() { return this.props.account.status !== 'ok'; } _shouldShowFooter(props: LoginPropTypes) { const { status } = props.account; return (status === 'none' || status === 'failed') && !this._shouldShowAccountHistory(props); } _getFooterAnimation(toValue: number) { return Animated.timing(this.state.animatedFooterValue, { toValue: toValue, easing: Animated.Easing.InOut(), duration: 250, useNativeDriver: false, }); } _onFooterLayout = (layout) => { this.setState({ footerHeight: layout.height }); }; _getLoginButtonAnimation(toValue: number) { return Animated.timing(this.state.animatedLoginButtonValue, { toValue: toValue, easing: Animated.Easing.Linear(), duration: 250, useNativeDriver: false, }); } _onSelectAccountFromHistory = (accountToken) => { this.props.onAccountTokenChange(accountToken); this.props.onLogin(accountToken); }; _createLoginForm() { const { accountHistory, accountToken } = this.props.account; // auto-focus on account input when failed to log in // do not refactor this into instance method, // it has to be new function each time to be called on each render const autoFocusOnFailure = (input) => { if (this.props.account.status === 'failed' && input) { input.focus(); } }; return ( {this._formSubtitle()} { } ); } _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)} /> ); } }