diff --git a/native/account/logged-out-modal.react.js b/native/account/logged-out-modal.react.js index 8148d9bae..5e267950d 100644 --- a/native/account/logged-out-modal.react.js +++ b/native/account/logged-out-modal.react.js @@ -1,790 +1,823 @@ // @flow import type { DispatchActionPayload } from 'lib/utils/action-utils'; import type { Dispatch } from 'lib/types/redux-types'; import type { AppState } from '../redux/redux-setup'; import type { KeyboardEvent, EmitterSubscription } from '../keyboard/keyboard'; import type { LogInState } from './log-in-panel.react'; import type { RegisterState } from './register-panel.react'; import { type DimensionsInfo, dimensionsInfoPropType, } from '../redux/dimensions-updater.react'; import type { ImageStyle } from '../types/styles'; import * as React from 'react'; import { View, StyleSheet, Text, TouchableOpacity, Image, Keyboard, Platform, BackHandler, ActivityIndicator, } from 'react-native'; import invariant from 'invariant'; import Icon from 'react-native-vector-icons/FontAwesome'; import PropTypes from 'prop-types'; import _isEqual from 'lodash/fp/isEqual'; import { SafeAreaView } from 'react-native-safe-area-context'; import Animated, { Easing } from 'react-native-reanimated'; import { fetchNewCookieFromNativeCredentials } from 'lib/utils/action-utils'; import { appStartNativeCredentialsAutoLogIn, appStartReduxLoggedInButInvalidCookie, } from 'lib/actions/user-actions'; import { connect } from 'lib/utils/redux-utils'; import { isLoggedIn } from 'lib/selectors/user-selectors'; import LogInPanelContainer from './log-in-panel-container.react'; import RegisterPanel from './register-panel.react'; import ConnectedStatusBar from '../connected-status-bar.react'; import { createIsForegroundSelector } from '../navigation/nav-selectors'; import { resetUserStateActionType } from '../redux/action-types'; import { splashBackgroundURI } from './background-info'; import { splashStyleSelector } from '../splash'; import { addKeyboardShowListener, addKeyboardDismissListener, removeKeyboardListener, } from '../keyboard/keyboard'; import { type StateContainer, type StateChange, setStateForContainer, } from '../utils/state-container'; import { LoggedOutModalRouteName } from '../navigation/route-names'; import { connectNav, type NavContextType, navContextPropType, } from '../navigation/navigation-context'; import { runTiming, ratchetAlongWithKeyboardHeight, } from '../utils/animation-utils'; let initialAppLoad = true; const safeAreaEdges = ['top', 'bottom']; /* eslint-disable import/no-named-as-default-member */ const { Value, Clock, block, set, call, cond, not, and, eq, neq, - greaterThan, lessThan, greaterOrEq, add, sub, divide, max, stopClock, clockRunning, } = Animated; /* eslint-enable import/no-named-as-default-member */ type LoggedOutMode = 'loading' | 'prompt' | 'log-in' | 'register'; const modeNumbers: { [LoggedOutMode]: number } = { 'loading': 0, 'prompt': 1, 'log-in': 2, 'register': 3, }; +function isPastPrompt(modeValue: Animated.Node) { + return and( + neq(modeValue, modeNumbers['loading']), + neq(modeValue, modeNumbers['prompt']), + ); +} type Props = { // Navigation state isForeground: boolean, navContext: ?NavContextType, // Redux state rehydrateConcluded: boolean, cookie: ?string, urlPrefix: string, loggedIn: boolean, dimensions: DimensionsInfo, splashStyle: ImageStyle, // Redux dispatch functions dispatch: Dispatch, dispatchActionPayload: DispatchActionPayload, }; type State = {| mode: LoggedOutMode, logInState: StateContainer, registerState: StateContainer, |}; class LoggedOutModal extends React.PureComponent { static propTypes = { isForeground: PropTypes.bool.isRequired, navContext: navContextPropType, rehydrateConcluded: PropTypes.bool.isRequired, cookie: PropTypes.string, urlPrefix: PropTypes.string.isRequired, loggedIn: PropTypes.bool.isRequired, dimensions: dimensionsInfoPropType.isRequired, dispatch: PropTypes.func.isRequired, dispatchActionPayload: PropTypes.func.isRequired, }; keyboardShowListener: ?EmitterSubscription; keyboardHideListener: ?EmitterSubscription; mounted = false; nextMode: LoggedOutMode = 'loading'; activeAlert = false; logInPanelContainer: ?LogInPanelContainer = null; contentHeight: Value; keyboardHeightValue = new Value(0); modeValue: Value; hideForgotPasswordLink = new Value(0); buttonOpacity: Value; panelPaddingTopValue: Value; footerPaddingTopValue: Value; panelOpacityValue: Value; forgotPasswordLinkOpacityValue: Value; constructor(props: Props) { super(props); // Man, this is a lot of boilerplate just to containerize some state. // Mostly due to Flow typing requirements... const setLogInState = setStateForContainer( this.guardedSetState, (change: $Shape) => (fullState: State) => ({ logInState: { ...fullState.logInState, state: { ...fullState.logInState.state, ...change }, }, }), ); const setRegisterState = setStateForContainer( this.guardedSetState, (change: $Shape) => (fullState: State) => ({ registerState: { ...fullState.registerState, state: { ...fullState.registerState.state, ...change }, }, }), ); this.state = { mode: props.rehydrateConcluded ? 'prompt' : 'loading', logInState: { state: { usernameOrEmailInputText: '', passwordInputText: '', }, setState: setLogInState, }, registerState: { state: { usernameInputText: '', emailInputText: '', passwordInputText: '', confirmPasswordInputText: '', }, setState: setRegisterState, }, }; if (props.rehydrateConcluded) { this.nextMode = 'prompt'; } const { height: windowHeight, topInset, bottomInset } = props.dimensions; this.contentHeight = new Value(windowHeight - topInset - bottomInset); this.modeValue = new Value(modeNumbers[this.nextMode]); this.buttonOpacity = new Value(props.rehydrateConcluded ? 1 : 0); this.panelPaddingTopValue = this.panelPaddingTop(); this.footerPaddingTopValue = this.footerPaddingTop(); this.panelOpacityValue = this.panelOpacity(); this.forgotPasswordLinkOpacityValue = this.forgotPasswordLinkOpacity(); } guardedSetState = (change: StateChange) => { if (this.mounted) { this.setState(change); } }; setMode(newMode: LoggedOutMode) { this.nextMode = newMode; this.guardedSetState({ mode: newMode }); this.modeValue.setValue(modeNumbers[newMode]); } proceedToNextMode = () => { this.guardedSetState({ mode: this.nextMode }); }; componentDidMount() { this.mounted = true; if (this.props.rehydrateConcluded) { this.onInitialAppLoad(); } if (this.props.isForeground) { this.onForeground(); } } componentWillUnmount() { this.mounted = false; if (this.props.isForeground) { this.onBackground(); } } componentDidUpdate(prevProps: Props, prevState: State) { if (!prevProps.rehydrateConcluded && this.props.rehydrateConcluded) { this.setMode('prompt'); this.onInitialAppLoad(); } if (!prevProps.isForeground && this.props.isForeground) { this.onForeground(); } else if (prevProps.isForeground && !this.props.isForeground) { this.onBackground(); } if (this.state.mode === 'prompt' && prevState.mode !== 'prompt') { this.buttonOpacity.setValue(0); Animated.timing(this.buttonOpacity, { easing: Easing.out(Easing.ease), duration: 250, toValue: 1.0, }).start(); } const newContentHeight = this.props.dimensions.height - this.props.dimensions.topInset - this.props.dimensions.bottomInset; const oldContentHeight = prevProps.dimensions.height - prevProps.dimensions.topInset - prevProps.dimensions.bottomInset; if (newContentHeight !== oldContentHeight) { this.contentHeight.setValue(newContentHeight); } } onForeground() { this.keyboardShowListener = addKeyboardShowListener(this.keyboardShow); this.keyboardHideListener = addKeyboardDismissListener(this.keyboardHide); BackHandler.addEventListener('hardwareBackPress', this.hardwareBack); } onBackground() { if (this.keyboardShowListener) { removeKeyboardListener(this.keyboardShowListener); this.keyboardShowListener = null; } if (this.keyboardHideListener) { removeKeyboardListener(this.keyboardHideListener); this.keyboardHideListener = null; } BackHandler.removeEventListener('hardwareBackPress', this.hardwareBack); } // This gets triggered when an app is killed and restarted // Not when it is returned from being backgrounded async onInitialAppLoad() { if (!initialAppLoad) { return; } initialAppLoad = false; const { loggedIn, cookie, urlPrefix, dispatch } = this.props; const hasUserCookie = cookie && cookie.startsWith('user='); if (loggedIn && hasUserCookie) { return; } if (!__DEV__) { const actionSource = loggedIn ? appStartReduxLoggedInButInvalidCookie : appStartNativeCredentialsAutoLogIn; const sessionChange = await fetchNewCookieFromNativeCredentials( dispatch, cookie, urlPrefix, actionSource, ); if ( sessionChange && sessionChange.cookie && sessionChange.cookie.startsWith('user=') ) { // success! we can expect subsequent actions to fix up the state return; } } if (loggedIn || hasUserCookie) { this.props.dispatchActionPayload(resetUserStateActionType, null); } } hardwareBack = () => { if (this.nextMode === 'log-in') { invariant(this.logInPanelContainer, 'ref should be set'); const returnValue = this.logInPanelContainer.backFromLogInMode(); if (returnValue) { return true; } } if (this.nextMode !== 'prompt') { this.goBackToPrompt(); return true; } return false; }; panelPaddingTop() { const headerHeight = Platform.OS === 'ios' ? 62.33 : 58.54; + const promptButtonsSize = Platform.OS === 'ios' ? 40 : 61; + const logInContainerSize = 165; + const registerPanelSize = 246; + + // On large enough devices, we want to properly center the panels on screen. + // But on smaller devices, this can lead to some issues: + // - We need enough space below the log-in panel to render the + // "Forgot password?" link + // - On Android, ratchetAlongWithKeyboardHeight won't adjust the panel's + // position when the keyboard size changes + // To address these issues, we artifically increase the panel sizes so that + // they get positioned a little higher than center on small devices. + const smallDeviceThreshold = 600; + const smallDeviceLogInContainerSize = 195; + const smallDeviceRegisterPanelSize = 261; + const containerSize = add( headerHeight, + cond(not(isPastPrompt(this.modeValue)), promptButtonsSize, 0), cond( - eq(this.modeValue, 2), - // We make space for the reset password button on smaller devices - cond(lessThan(this.contentHeight, 600), 195, 165), + eq(this.modeValue, modeNumbers['log-in']), + cond( + lessThan(this.contentHeight, smallDeviceThreshold), + smallDeviceLogInContainerSize, + logInContainerSize, + ), 0, ), cond( - eq(this.modeValue, 3), - // We make space for the password manager on smaller devices - cond(lessThan(this.contentHeight, 600), 261, 246), + eq(this.modeValue, modeNumbers['register']), + cond( + lessThan(this.contentHeight, smallDeviceThreshold), + smallDeviceRegisterPanelSize, + registerPanelSize, + ), 0, ), - cond(lessThan(this.modeValue, 2), Platform.OS === 'ios' ? 40 : 61, 0), ); const potentialPanelPaddingTop = divide( max(sub(this.contentHeight, this.keyboardHeightValue, containerSize), 0), 2, ); const panelPaddingTop = new Value(-1); const targetPanelPaddingTop = new Value(-1); const prevModeValue = new Value(modeNumbers[this.nextMode]); const clock = new Clock(); const keyboardTimeoutClock = new Clock(); return block([ cond(lessThan(panelPaddingTop, 0), [ set(panelPaddingTop, potentialPanelPaddingTop), set(targetPanelPaddingTop, potentialPanelPaddingTop), ]), cond( lessThan(this.keyboardHeightValue, 0), [ runTiming(keyboardTimeoutClock, 0, 1, true, { duration: 500 }), cond( not(clockRunning(keyboardTimeoutClock)), set(this.keyboardHeightValue, 0), ), ], stopClock(keyboardTimeoutClock), ), cond( and( greaterOrEq(this.keyboardHeightValue, 0), neq(prevModeValue, this.modeValue), ), [ stopClock(clock), cond( - neq(greaterThan(prevModeValue, 1), greaterThan(this.modeValue, 1)), + neq(isPastPrompt(prevModeValue), isPastPrompt(this.modeValue)), set(targetPanelPaddingTop, potentialPanelPaddingTop), ), set(prevModeValue, this.modeValue), ], ), ratchetAlongWithKeyboardHeight(this.keyboardHeightValue, [ stopClock(clock), set(targetPanelPaddingTop, potentialPanelPaddingTop), ]), cond( neq(panelPaddingTop, targetPanelPaddingTop), set( panelPaddingTop, runTiming(clock, panelPaddingTop, targetPanelPaddingTop), ), ), panelPaddingTop, ]); } footerPaddingTop() { const textHeight = Platform.OS === 'ios' ? 17 : 19; + const spacingAboveKeyboard = 15; const targetFooterPaddingTop = max( - sub(this.contentHeight, max(this.keyboardHeightValue, 0), textHeight, 15), + sub( + this.contentHeight, + max(this.keyboardHeightValue, 0), + textHeight, + spacingAboveKeyboard, + ), 0, ); const footerPaddingTop = new Value(-1); const prevTargetFooterPaddingTop = new Value(-1); const clock = new Clock(); return block([ cond(lessThan(footerPaddingTop, 0), [ set(footerPaddingTop, targetFooterPaddingTop), set(prevTargetFooterPaddingTop, targetFooterPaddingTop), ]), cond(greaterOrEq(this.keyboardHeightValue, 0), [ cond(neq(targetFooterPaddingTop, prevTargetFooterPaddingTop), [ stopClock(clock), set(prevTargetFooterPaddingTop, targetFooterPaddingTop), ]), cond( neq(footerPaddingTop, targetFooterPaddingTop), set( footerPaddingTop, runTiming(clock, footerPaddingTop, targetFooterPaddingTop), ), ), ]), footerPaddingTop, ]); } panelOpacity() { - const targetPanelOpacity = greaterThan(this.modeValue, 1); + const targetPanelOpacity = isPastPrompt(this.modeValue); const panelOpacity = new Value(-1); const prevPanelOpacity = new Value(-1); const prevTargetPanelOpacity = new Value(-1); const clock = new Clock(); return block([ cond(lessThan(panelOpacity, 0), [ set(panelOpacity, targetPanelOpacity), set(prevPanelOpacity, targetPanelOpacity), set(prevTargetPanelOpacity, targetPanelOpacity), ]), cond(greaterOrEq(this.keyboardHeightValue, 0), [ cond(neq(targetPanelOpacity, prevTargetPanelOpacity), [ stopClock(clock), set(prevTargetPanelOpacity, targetPanelOpacity), ]), cond( neq(panelOpacity, targetPanelOpacity), set(panelOpacity, runTiming(clock, panelOpacity, targetPanelOpacity)), ), ]), cond( and(eq(panelOpacity, 0), neq(prevPanelOpacity, 0)), call([], this.proceedToNextMode), ), set(prevPanelOpacity, panelOpacity), panelOpacity, ]); } forgotPasswordLinkOpacity() { const targetForgotPasswordLinkOpacity = and( - eq(this.modeValue, 2), + eq(this.modeValue, modeNumbers['log-in']), not(this.hideForgotPasswordLink), ); const forgotPasswordLinkOpacity = new Value(0); const prevTargetForgotPasswordLinkOpacity = new Value(0); const clock = new Clock(); return block([ cond(greaterOrEq(this.keyboardHeightValue, 0), [ cond( neq( targetForgotPasswordLinkOpacity, prevTargetForgotPasswordLinkOpacity, ), [ stopClock(clock), set( prevTargetForgotPasswordLinkOpacity, targetForgotPasswordLinkOpacity, ), ], ), cond( neq(forgotPasswordLinkOpacity, targetForgotPasswordLinkOpacity), set( forgotPasswordLinkOpacity, runTiming( clock, forgotPasswordLinkOpacity, targetForgotPasswordLinkOpacity, ), ), ), ]), forgotPasswordLinkOpacity, ]); } keyboardShow = (event: KeyboardEvent) => { if (_isEqual(event.startCoordinates)(event.endCoordinates)) { return; } const keyboardHeight = Platform.select({ // Android doesn't include the bottomInset in this height measurement android: event.endCoordinates.height, default: Math.max( event.endCoordinates.height - this.props.dimensions.bottomInset, 0, ), }); this.keyboardHeightValue.setValue(keyboardHeight); }; keyboardHide = () => { if (!this.activeAlert) { this.keyboardHeightValue.setValue(0); } }; setActiveAlert = (activeAlert: boolean) => { this.activeAlert = activeAlert; }; goBackToPrompt = () => { this.nextMode = 'prompt'; this.keyboardHeightValue.setValue(0); this.modeValue.setValue(modeNumbers['prompt']); Keyboard.dismiss(); }; render() { let panel = null; let buttons = null; if (this.state.mode === 'log-in') { panel = ( ); } else if (this.state.mode === 'register') { panel = ( ); } else if (this.state.mode === 'prompt') { const opacityStyle = { opacity: this.buttonOpacity }; buttons = ( LOG IN SIGN UP ); } else if (this.state.mode === 'loading') { panel = ( ); } let forgotPasswordLink = null; if (this.state.mode === 'log-in') { const reanimatedStyle = { top: this.footerPaddingTopValue, opacity: this.forgotPasswordLinkOpacityValue, }; forgotPasswordLink = ( Forgot password? ); } const windowWidth = this.props.dimensions.width; const buttonStyle = { opacity: this.panelOpacityValue, left: windowWidth < 360 ? 28 : 40, }; const padding = { paddingTop: this.panelPaddingTopValue }; const animatedContent = ( SquadCal {panel} ); const backgroundSource = { uri: splashBackgroundURI }; return ( {animatedContent} {buttons} {forgotPasswordLink} ); } logInPanelContainerRef = (logInPanelContainer: ?LogInPanelContainer) => { this.logInPanelContainer = logInPanelContainer; }; onPressLogIn = () => { if (Platform.OS !== 'ios') { // For some strange reason, iOS's password management logic doesn't // realize that the username and password fields in LogInPanel are related // if the username field gets focused on mount. To avoid this issue we // need the username and password fields to both appear on-screen before // we focus the username field. However, when we set keyboardHeightValue // to -1 here, we are telling our Reanimated logic to wait until the // keyboard appears before showing LogInPanel. Since we need LogInPanel // to appear before the username field is focused, we need to avoid this // behavior on iOS. this.keyboardHeightValue.setValue(-1); } this.setMode('log-in'); }; onPressRegister = () => { this.keyboardHeightValue.setValue(-1); this.setMode('register'); }; onPressForgotPassword = () => { invariant(this.logInPanelContainer, 'ref should be set'); this.logInPanelContainer.onPressForgotPassword(); }; } const styles = StyleSheet.create({ animationContainer: { flex: 1, }, backButton: { position: 'absolute', top: 13, }, button: { backgroundColor: '#FFFFFFAA', borderRadius: 6, marginBottom: 10, marginLeft: 40, marginRight: 40, marginTop: 10, paddingBottom: 6, paddingLeft: 18, paddingRight: 18, paddingTop: 6, }, buttonContainer: { bottom: 0, left: 0, paddingBottom: 20, position: 'absolute', right: 0, }, buttonText: { color: '#000000FF', fontFamily: 'OpenSans-Semibold', fontSize: 22, textAlign: 'center', }, container: { backgroundColor: 'transparent', flex: 1, }, forgotPasswordText: { color: '#8899FF', }, forgotPasswordTextContainer: { alignSelf: 'flex-end', position: 'absolute', right: 20, }, header: { color: 'white', fontFamily: 'Anaheim-Regular', fontSize: 48, textAlign: 'center', }, loadingIndicator: { paddingTop: 15, }, modalBackground: { bottom: 0, left: 0, position: 'absolute', right: 0, top: 0, }, }); const isForegroundSelector = createIsForegroundSelector( LoggedOutModalRouteName, ); export default connectNav((context: ?NavContextType) => ({ isForeground: isForegroundSelector(context), navContext: context, }))( connect( (state: AppState, ownProps: { navContext: ?NavContextType }) => ({ rehydrateConcluded: !!( state._persist && state._persist.rehydrated && ownProps.navContext ), cookie: state.cookie, urlPrefix: state.urlPrefix, loggedIn: isLoggedIn(state), dimensions: state.dimensions, splashStyle: splashStyleSelector(state), }), null, true, )(LoggedOutModal), ); diff --git a/native/account/verification-modal.react.js b/native/account/verification-modal.react.js index 4caa773f2..7a766981e 100644 --- a/native/account/verification-modal.react.js +++ b/native/account/verification-modal.react.js @@ -1,572 +1,582 @@ // @flow import type { AppState } from '../redux/redux-setup'; import type { DispatchActionPromise } from 'lib/utils/action-utils'; import { type VerifyField, verifyField, type HandleVerificationCodeResult, } from 'lib/types/verify-types'; import type { KeyboardEvent } from '../keyboard/keyboard'; import { type DimensionsInfo, dimensionsInfoPropType, } from '../redux/dimensions-updater.react'; import type { ImageStyle } from '../types/styles'; import type { RootNavigationProp } from '../navigation/root-navigator.react'; import type { NavigationRoute } from '../navigation/route-names'; import * as React from 'react'; import { Image, Text, View, StyleSheet, ActivityIndicator, Platform, Keyboard, TouchableHighlight, } from 'react-native'; import Icon from 'react-native-vector-icons/FontAwesome'; import invariant from 'invariant'; import PropTypes from 'prop-types'; import { SafeAreaView } from 'react-native-safe-area-context'; import Animated from 'react-native-reanimated'; import { registerFetchKey } from 'lib/reducers/loading-reducer'; import { connect } from 'lib/utils/redux-utils'; import { handleVerificationCodeActionTypes, handleVerificationCode, } from 'lib/actions/user-actions'; import sleep from 'lib/utils/sleep'; import ConnectedStatusBar from '../connected-status-bar.react'; import ResetPasswordPanel from './reset-password-panel.react'; import { createIsForegroundSelector } from '../navigation/nav-selectors'; import { splashBackgroundURI } from './background-info'; import { splashStyleSelector } from '../splash'; import { addKeyboardShowListener, addKeyboardDismissListener, removeKeyboardListener, } from '../keyboard/keyboard'; import { VerificationModalRouteName } from '../navigation/route-names'; import { connectNav, type NavContextType, } from '../navigation/navigation-context'; import { runTiming, ratchetAlongWithKeyboardHeight, } from '../utils/animation-utils'; const safeAreaEdges = ['top', 'bottom']; /* eslint-disable import/no-named-as-default-member */ const { Value, Clock, block, set, call, cond, not, and, eq, neq, - greaterThan, lessThan, greaterOrEq, sub, divide, max, stopClock, clockRunning, } = Animated; /* eslint-enable import/no-named-as-default-member */ export type VerificationModalParams = {| verifyCode: string, |}; type VerificationModalMode = 'simple-text' | 'reset-password'; const modeNumbers: { [VerificationModalMode]: number } = { 'simple-text': 0, 'reset-password': 1, }; type Props = { navigation: RootNavigationProp<'VerificationModal'>, route: NavigationRoute<'VerificationModal'>, // Navigation state isForeground: boolean, // Redux state dimensions: DimensionsInfo, splashStyle: ImageStyle, // Redux dispatch functions dispatchActionPromise: DispatchActionPromise, // async functions that hit server APIs handleVerificationCode: ( code: string, ) => Promise, }; type State = {| mode: VerificationModalMode, verifyField: ?VerifyField, errorMessage: ?string, resetPasswordUsername: ?string, |}; class VerificationModal extends React.PureComponent { static propTypes = { navigation: PropTypes.shape({ clearRootModals: PropTypes.func.isRequired, }).isRequired, route: PropTypes.shape({ key: PropTypes.string.isRequired, params: PropTypes.shape({ verifyCode: PropTypes.string.isRequired, }).isRequired, }).isRequired, isForeground: PropTypes.bool.isRequired, dimensions: dimensionsInfoPropType.isRequired, dispatchActionPromise: PropTypes.func.isRequired, handleVerificationCode: PropTypes.func.isRequired, }; keyboardShowListener: ?Object; keyboardHideListener: ?Object; activeAlert = false; nextMode: VerificationModalMode = 'simple-text'; contentHeight: Value; keyboardHeightValue = new Value(0); modeValue: Value; paddingTopValue: Value; resetPasswordPanelOpacityValue: Value; constructor(props: Props) { super(props); this.state = { mode: 'simple-text', verifyField: null, errorMessage: null, resetPasswordUsername: null, }; const { height: windowHeight, topInset, bottomInset } = props.dimensions; this.contentHeight = new Value(windowHeight - topInset - bottomInset); this.modeValue = new Value(modeNumbers[this.nextMode]); this.paddingTopValue = this.paddingTop(); this.resetPasswordPanelOpacityValue = this.resetPasswordPanelOpacity(); } proceedToNextMode = () => { this.setState({ mode: this.nextMode }); }; paddingTop() { + const simpleTextHeight = 90; + const resetPasswordPanelHeight = 165; const potentialPaddingTop = divide( max( sub( this.contentHeight, - cond(eq(this.modeValue, 0), 90), - cond(eq(this.modeValue, 1), 165), + cond( + eq(this.modeValue, modeNumbers['simple-text']), + simpleTextHeight, + ), + cond( + eq(this.modeValue, modeNumbers['reset-password']), + resetPasswordPanelHeight, + ), this.keyboardHeightValue, ), 0, ), 2, ); const paddingTop = new Value(-1); const targetPaddingTop = new Value(-1); const prevModeValue = new Value(modeNumbers[this.nextMode]); const clock = new Clock(); const keyboardTimeoutClock = new Clock(); return block([ cond(lessThan(paddingTop, 0), [ set(paddingTop, potentialPaddingTop), set(targetPaddingTop, potentialPaddingTop), ]), cond( lessThan(this.keyboardHeightValue, 0), [ runTiming(keyboardTimeoutClock, 0, 1, true, { duration: 500 }), cond( not(clockRunning(keyboardTimeoutClock)), set(this.keyboardHeightValue, 0), ), ], stopClock(keyboardTimeoutClock), ), cond( and( greaterOrEq(this.keyboardHeightValue, 0), neq(prevModeValue, this.modeValue), ), [ stopClock(clock), set(targetPaddingTop, potentialPaddingTop), set(prevModeValue, this.modeValue), ], ), ratchetAlongWithKeyboardHeight(this.keyboardHeightValue, [ stopClock(clock), set(targetPaddingTop, potentialPaddingTop), ]), cond( neq(paddingTop, targetPaddingTop), set(paddingTop, runTiming(clock, paddingTop, targetPaddingTop)), ), paddingTop, ]); } resetPasswordPanelOpacity() { - const targetResetPasswordPanelOpacity = greaterThan(this.modeValue, 0); + const targetResetPasswordPanelOpacity = eq( + this.modeValue, + modeNumbers['reset-password'], + ); const resetPasswordPanelOpacity = new Value(-1); const prevResetPasswordPanelOpacity = new Value(-1); const prevTargetResetPasswordPanelOpacity = new Value(-1); const clock = new Clock(); return block([ cond(lessThan(resetPasswordPanelOpacity, 0), [ set(resetPasswordPanelOpacity, targetResetPasswordPanelOpacity), set(prevResetPasswordPanelOpacity, targetResetPasswordPanelOpacity), set( prevTargetResetPasswordPanelOpacity, targetResetPasswordPanelOpacity, ), ]), cond(greaterOrEq(this.keyboardHeightValue, 0), [ cond( neq( targetResetPasswordPanelOpacity, prevTargetResetPasswordPanelOpacity, ), [ stopClock(clock), set( prevTargetResetPasswordPanelOpacity, targetResetPasswordPanelOpacity, ), ], ), cond( neq(resetPasswordPanelOpacity, targetResetPasswordPanelOpacity), set( resetPasswordPanelOpacity, runTiming( clock, resetPasswordPanelOpacity, targetResetPasswordPanelOpacity, ), ), ), ]), cond( and( eq(resetPasswordPanelOpacity, 0), neq(prevResetPasswordPanelOpacity, 0), ), call([], this.proceedToNextMode), ), set(prevResetPasswordPanelOpacity, resetPasswordPanelOpacity), resetPasswordPanelOpacity, ]); } componentDidMount() { this.props.dispatchActionPromise( handleVerificationCodeActionTypes, this.handleVerificationCodeAction(), ); Keyboard.dismiss(); if (this.props.isForeground) { this.onForeground(); } } componentDidUpdate(prevProps: Props, prevState: State) { if ( this.state.verifyField === verifyField.EMAIL && prevState.verifyField !== verifyField.EMAIL ) { sleep(1500).then(this.dismiss); } const prevCode = prevProps.route.params.verifyCode; const code = this.props.route.params.verifyCode; if (code !== prevCode) { Keyboard.dismiss(); this.nextMode = 'simple-text'; this.modeValue.setValue(modeNumbers[this.nextMode]); this.keyboardHeightValue.setValue(0); this.setState({ mode: this.nextMode, verifyField: null, errorMessage: null, resetPasswordUsername: null, }); this.props.dispatchActionPromise( handleVerificationCodeActionTypes, this.handleVerificationCodeAction(), ); } if (this.props.isForeground && !prevProps.isForeground) { this.onForeground(); } else if (!this.props.isForeground && prevProps.isForeground) { this.onBackground(); } const newContentHeight = this.props.dimensions.height - this.props.dimensions.topInset - this.props.dimensions.bottomInset; const oldContentHeight = prevProps.dimensions.height - prevProps.dimensions.topInset - prevProps.dimensions.bottomInset; if (newContentHeight !== oldContentHeight) { this.contentHeight.setValue(newContentHeight); } } onForeground() { this.keyboardShowListener = addKeyboardShowListener(this.keyboardShow); this.keyboardHideListener = addKeyboardDismissListener(this.keyboardHide); } onBackground() { if (this.keyboardShowListener) { removeKeyboardListener(this.keyboardShowListener); this.keyboardShowListener = null; } if (this.keyboardHideListener) { removeKeyboardListener(this.keyboardHideListener); this.keyboardHideListener = null; } } dismiss = () => { this.props.navigation.clearRootModals([this.props.route.key]); }; onResetPasswordSuccess = async () => { this.nextMode = 'simple-text'; this.modeValue.setValue(modeNumbers[this.nextMode]); this.keyboardHeightValue.setValue(0); Keyboard.dismiss(); // Wait a couple seconds before letting the SUCCESS action propagate and // clear VerificationModal await sleep(1750); this.dismiss(); }; async handleVerificationCodeAction() { const code = this.props.route.params.verifyCode; try { const result = await this.props.handleVerificationCode(code); if (result.verifyField === verifyField.EMAIL) { this.setState({ verifyField: result.verifyField }); } else if (result.verifyField === verifyField.RESET_PASSWORD) { this.nextMode = 'reset-password'; this.modeValue.setValue(modeNumbers[this.nextMode]); this.keyboardHeightValue.setValue(-1); this.setState({ verifyField: result.verifyField, mode: 'reset-password', resetPasswordUsername: result.resetPasswordUsername, }); } } catch (e) { if (e.message === 'invalid_code') { this.setState({ errorMessage: 'Invalid verification code' }); } else { this.setState({ errorMessage: 'Unknown error occurred' }); } throw e; } } keyboardShow = (event: KeyboardEvent) => { const keyboardHeight = Platform.select({ // Android doesn't include the bottomInset in this height measurement android: event.endCoordinates.height, default: Math.max( event.endCoordinates.height - this.props.dimensions.bottomInset, 0, ), }); this.keyboardHeightValue.setValue(keyboardHeight); }; keyboardHide = () => { if (!this.activeAlert) { this.keyboardHeightValue.setValue(0); } }; setActiveAlert = (activeAlert: boolean) => { this.activeAlert = activeAlert; }; render() { const statusBar = ; const background = ( ); const closeButton = ( ); let content; if (this.state.mode === 'reset-password') { const code = this.props.route.params.verifyCode; invariant(this.state.resetPasswordUsername, 'should be set'); content = ( ); } else if (this.state.errorMessage) { content = ( {this.state.errorMessage} ); } else if (this.state.verifyField !== null) { let message; if (this.state.verifyField === verifyField.EMAIL) { message = 'Thanks for verifying your email!'; } else { message = 'Your password has been reset.'; } content = ( {message} ); } else { content = ( Verifying code... ); } const padding = { paddingTop: this.paddingTopValue }; const animatedContent = ( {content} ); return ( {background} {statusBar} {animatedContent} {closeButton} ); } } const styles = StyleSheet.create({ closeButton: { backgroundColor: '#D0D0D055', borderRadius: 3, height: 36, position: 'absolute', right: 15, top: 15, width: 36, }, closeButtonIcon: { left: 10, position: 'absolute', top: 8, }, container: { backgroundColor: 'transparent', flex: 1, }, contentContainer: { height: 90, }, icon: { textAlign: 'center', }, loadingText: { bottom: 0, color: 'white', fontSize: 20, left: 0, position: 'absolute', right: 0, textAlign: 'center', }, modalBackground: { height: ('100%': number | string), position: 'absolute', width: ('100%': number | string), }, }); registerFetchKey(handleVerificationCodeActionTypes); const isForegroundSelector = createIsForegroundSelector( VerificationModalRouteName, ); export default connectNav((context: ?NavContextType) => ({ isForeground: isForegroundSelector(context), }))( connect( (state: AppState) => ({ dimensions: state.dimensions, splashStyle: splashStyleSelector(state), }), { handleVerificationCode }, )(VerificationModal), );