Page MenuHomePhabricator

No OneTemporary

diff --git a/native/account/logged-out-modal.react.js b/native/account/logged-out-modal.react.js
index 273778d95..769c0c9d6 100644
--- a/native/account/logged-out-modal.react.js
+++ b/native/account/logged-out-modal.react.js
@@ -1,803 +1,810 @@
// @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 OnePassword from 'react-native-onepassword';
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 } from '../utils/animation-utils';
+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';
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,
onePasswordSupported: boolean,
logInState: StateContainer<LogInState>,
registerState: StateContainer<RegisterState>,
|};
class LoggedOutModal extends React.PureComponent<Props, State> {
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);
this.determineOnePasswordSupport();
// 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<LogInState>) => (fullState: State) => ({
logInState: {
...fullState.logInState,
state: { ...fullState.logInState.state, ...change },
},
}),
);
const setRegisterState = setStateForContainer(
this.guardedSetState,
(change: $Shape<RegisterState>) => (fullState: State) => ({
registerState: {
...fullState.registerState,
state: { ...fullState.registerState.state, ...change },
},
}),
);
this.state = {
mode: props.rehydrateConcluded ? 'prompt' : 'loading',
onePasswordSupported: false,
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(LoggedOutModal.getModeNumber(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<State>) => {
if (this.mounted) {
this.setState(change);
}
};
setMode(newMode: LoggedOutMode) {
this.nextMode = newMode;
this.guardedSetState({ mode: newMode });
this.modeValue.setValue(LoggedOutModal.getModeNumber(newMode));
}
proceedToNextMode = () => {
this.guardedSetState({ mode: this.nextMode });
};
static getModeNumber(mode: LoggedOutMode) {
if (mode === 'loading') {
return 0;
} else if (mode === 'prompt') {
return 1;
} else if (mode === 'log-in') {
return 2;
} else if (mode === 'register') {
return 3;
}
invariant(false, `${mode} is not a valid LoggedOutMode`);
}
async determineOnePasswordSupport() {
let onePasswordSupported;
try {
onePasswordSupported = await OnePassword.isSupported();
} catch (e) {
onePasswordSupported = false;
}
this.guardedSetState({ onePasswordSupported });
}
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 containerSize = add(
headerHeight,
cond(
eq(this.modeValue, 2),
// We make space for the reset password button on smaller devices
cond(lessThan(this.contentHeight, 600), 195, 165),
0,
),
cond(
eq(this.modeValue, 3),
// We make space for the password manager on smaller devices
cond(lessThan(this.contentHeight, 600), 261, 246),
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(
LoggedOutModal.getModeNumber(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)),
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 targetFooterPaddingTop = max(
sub(this.contentHeight, max(this.keyboardHeightValue, 0), textHeight, 15),
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 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),
not(this.hideForgotPasswordLink),
);
const forgotPasswordLinkOpacity = new Value(-1);
const prevTargetForgotPasswordLinkOpacity = new Value(-1);
const clock = new Clock();
return block([
cond(lessThan(forgotPasswordLinkOpacity, 0), [
set(forgotPasswordLinkOpacity, targetForgotPasswordLinkOpacity),
set(
prevTargetForgotPasswordLinkOpacity,
targetForgotPasswordLinkOpacity,
),
]),
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(LoggedOutModal.getModeNumber('prompt'));
Keyboard.dismiss();
};
render() {
let panel = null;
let buttons = null;
if (this.state.mode === 'log-in') {
panel = (
<LogInPanelContainer
onePasswordSupported={this.state.onePasswordSupported}
setActiveAlert={this.setActiveAlert}
opacityValue={this.panelOpacityValue}
hideForgotPasswordLink={this.hideForgotPasswordLink}
logInState={this.state.logInState}
innerRef={this.logInPanelContainerRef}
/>
);
} else if (this.state.mode === 'register') {
panel = (
<RegisterPanel
setActiveAlert={this.setActiveAlert}
opacityValue={this.panelOpacityValue}
onePasswordSupported={this.state.onePasswordSupported}
state={this.state.registerState}
/>
);
} else if (this.state.mode === 'prompt') {
const opacityStyle = { opacity: this.buttonOpacity };
buttons = (
<Animated.View style={[styles.buttonContainer, opacityStyle]}>
<TouchableOpacity
onPress={this.onPressLogIn}
style={styles.button}
activeOpacity={0.6}
>
<Text style={styles.buttonText}>LOG IN</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={this.onPressRegister}
style={styles.button}
activeOpacity={0.6}
>
<Text style={styles.buttonText}>SIGN UP</Text>
</TouchableOpacity>
</Animated.View>
);
} else if (this.state.mode === 'loading') {
panel = (
<ActivityIndicator
color="white"
size="large"
style={styles.loadingIndicator}
/>
);
}
let forgotPasswordLink = null;
if (this.state.mode === 'log-in') {
const reanimatedStyle = {
top: this.footerPaddingTopValue,
opacity: this.forgotPasswordLinkOpacityValue,
};
forgotPasswordLink = (
<Animated.View
style={[styles.forgotPasswordTextContainer, reanimatedStyle]}
>
<TouchableOpacity
activeOpacity={0.6}
onPress={this.onPressForgotPassword}
>
<Text style={styles.forgotPasswordText}>Forgot password?</Text>
</TouchableOpacity>
</Animated.View>
);
}
const windowWidth = this.props.dimensions.width;
const buttonStyle = {
opacity: this.panelOpacityValue,
left: windowWidth < 360 ? 28 : 40,
};
const padding = { paddingTop: this.panelPaddingTopValue };
const animatedContent = (
<Animated.View style={[styles.animationContainer, padding]}>
<View>
<Text style={styles.header}>SquadCal</Text>
<Animated.View style={[styles.backButton, buttonStyle]}>
<TouchableOpacity activeOpacity={0.6} onPress={this.hardwareBack}>
<Icon name="arrow-circle-o-left" size={36} color="#FFFFFFAA" />
</TouchableOpacity>
</Animated.View>
</View>
{panel}
</Animated.View>
);
const backgroundSource = { uri: splashBackgroundURI };
return (
<React.Fragment>
<ConnectedStatusBar barStyle="light-content" />
<Image
source={backgroundSource}
style={[styles.modalBackground, this.props.splashStyle]}
/>
<SafeAreaView style={styles.container} edges={safeAreaEdges}>
<View style={styles.container}>
{animatedContent}
{buttons}
{forgotPasswordLink}
</View>
</SafeAreaView>
</React.Fragment>
);
}
logInPanelContainerRef = (logInPanelContainer: ?LogInPanelContainer) => {
this.logInPanelContainer = logInPanelContainer;
};
onPressLogIn = () => {
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 1c1b193dd..36d1a07c9 100644
--- a/native/account/verification-modal.react.js
+++ b/native/account/verification-modal.react.js
@@ -1,587 +1,594 @@
// @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 OnePassword from 'react-native-onepassword';
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 } from '../utils/animation-utils';
+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';
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<HandleVerificationCodeResult>,
};
type State = {|
mode: VerificationModalMode,
verifyField: ?VerifyField,
errorMessage: ?string,
resetPasswordUsername: ?string,
onePasswordSupported: boolean,
|};
class VerificationModal extends React.PureComponent<Props, State> {
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,
onePasswordSupported: false,
};
const { height: windowHeight, topInset, bottomInset } = props.dimensions;
this.contentHeight = new Value(windowHeight - topInset - bottomInset);
this.modeValue = new Value(VerificationModal.getModeNumber(this.nextMode));
this.paddingTopValue = this.paddingTop();
this.resetPasswordPanelOpacityValue = this.resetPasswordPanelOpacity();
}
proceedToNextMode = () => {
this.setState({ mode: this.nextMode });
};
static getModeNumber(mode: VerificationModalMode) {
if (mode === 'simple-text') {
return 0;
} else if (mode === 'reset-password') {
return 1;
}
invariant(false, `${mode} is not a valid VerificationModalMode`);
}
paddingTop() {
const potentialPaddingTop = divide(
max(
sub(
this.contentHeight,
cond(eq(this.modeValue, 0), 90),
cond(eq(this.modeValue, 1), 165),
this.keyboardHeightValue,
),
0,
),
2,
);
const paddingTop = new Value(-1);
const targetPaddingTop = new Value(-1);
const prevModeValue = new Value(
VerificationModal.getModeNumber(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 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,
]);
}
async determineOnePasswordSupport() {
let onePasswordSupported;
try {
onePasswordSupported = await OnePassword.isSupported();
} catch (e) {
onePasswordSupported = false;
}
this.setState({ onePasswordSupported });
}
componentDidMount() {
this.determineOnePasswordSupport();
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(VerificationModal.getModeNumber(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(VerificationModal.getModeNumber(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(VerificationModal.getModeNumber(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 = <ConnectedStatusBar barStyle="light-content" />;
const background = (
<Image
source={{ uri: splashBackgroundURI }}
style={[styles.modalBackground, this.props.splashStyle]}
/>
);
const closeButton = (
<TouchableHighlight
onPress={this.dismiss}
style={styles.closeButton}
underlayColor="#A0A0A0DD"
>
<Icon
name="close"
size={20}
color="white"
style={styles.closeButtonIcon}
/>
</TouchableHighlight>
);
let content;
if (this.state.mode === 'reset-password') {
const code = this.props.route.params.verifyCode;
invariant(this.state.resetPasswordUsername, 'should be set');
content = (
<ResetPasswordPanel
verifyCode={code}
username={this.state.resetPasswordUsername}
onePasswordSupported={this.state.onePasswordSupported}
onSuccess={this.onResetPasswordSuccess}
setActiveAlert={this.setActiveAlert}
opacityValue={this.resetPasswordPanelOpacityValue}
/>
);
} else if (this.state.errorMessage) {
content = (
<View style={styles.contentContainer}>
<Icon
name="exclamation"
size={48}
color="#FF0000DD"
style={styles.icon}
/>
<Text style={styles.loadingText}>{this.state.errorMessage}</Text>
</View>
);
} 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 = (
<View style={styles.contentContainer}>
<Icon
name="check-circle"
size={48}
color="#88FF88DD"
style={styles.icon}
/>
<Text style={styles.loadingText}>{message}</Text>
</View>
);
} else {
content = (
<View style={styles.contentContainer}>
<ActivityIndicator color="white" size="large" />
<Text style={styles.loadingText}>Verifying code...</Text>
</View>
);
}
const padding = { paddingTop: this.paddingTopValue };
const animatedContent = (
<Animated.View style={padding}>{content}</Animated.View>
);
return (
<React.Fragment>
{background}
<SafeAreaView style={styles.container} edges={safeAreaEdges}>
<View style={styles.container}>
{statusBar}
{animatedContent}
{closeButton}
</View>
</SafeAreaView>
</React.Fragment>
);
}
}
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),
);
diff --git a/native/utils/animation-utils.js b/native/utils/animation-utils.js
index 62b78b6d2..a1b410742 100644
--- a/native/utils/animation-utils.js
+++ b/native/utils/animation-utils.js
@@ -1,96 +1,135 @@
// @flow
import Animated, { Easing } from 'react-native-reanimated';
import { State as GestureState } from 'react-native-gesture-handler';
+import { Platform } from 'react-native';
/* eslint-disable import/no-named-as-default-member */
const {
Clock,
Value,
+ block,
cond,
not,
+ and,
greaterThan,
+ lessThan,
eq,
sub,
set,
+ max,
startClock,
stopClock,
clockRunning,
timing,
} = Animated;
/* eslint-enable import/no-named-as-default-member */
function clamp(value: Value, minValue: Value, maxValue: Value): Value {
return cond(
greaterThan(value, maxValue),
maxValue,
cond(greaterThan(minValue, value), minValue, value),
);
}
function delta(value: Value) {
const prevValue = new Value(0);
const deltaValue = new Value(0);
return [
set(deltaValue, cond(eq(prevValue, 0), 0, sub(value, prevValue))),
set(prevValue, value),
deltaValue,
];
}
function gestureJustStarted(state: Value) {
const prevValue = new Value(-1);
return cond(eq(prevValue, state), 0, [
set(prevValue, state),
eq(state, GestureState.ACTIVE),
]);
}
function gestureJustEnded(state: Value) {
const prevValue = new Value(-1);
return cond(eq(prevValue, state), 0, [
set(prevValue, state),
eq(state, GestureState.END),
]);
}
const defaultTimingConfig = {
duration: 250,
easing: Easing.out(Easing.ease),
};
type TimingConfig = $Shape<typeof defaultTimingConfig>;
function runTiming(
clock: Clock,
initialValue: Value | number,
finalValue: Value | number,
startStopClock: boolean = true,
config: TimingConfig = defaultTimingConfig,
): Value {
const state = {
finished: new Value(0),
position: new Value(0),
frameTime: new Value(0),
time: new Value(0),
};
const timingConfig = {
...defaultTimingConfig,
...config,
toValue: new Value(0),
};
return [
cond(not(clockRunning(clock)), [
set(state.finished, 0),
set(state.frameTime, 0),
set(state.time, 0),
set(state.position, initialValue),
set(timingConfig.toValue, finalValue),
startStopClock && startClock(clock),
]),
timing(clock, state, timingConfig),
cond(state.finished, startStopClock && stopClock(clock)),
state.position,
];
}
-export { clamp, delta, gestureJustStarted, gestureJustEnded, runTiming };
+// You provide a node that performs a "ratchet",
+// and this function will call it as keyboard height increases
+function ratchetAlongWithKeyboardHeight(
+ keyboardHeight: Animated.Node,
+ ratchetFunction: Animated.Node,
+) {
+ const prevKeyboardHeightValue = new Value(-1);
+ const whenToUpdate = Platform.select({
+ // In certain situations, iOS will send multiple keyboardShows in rapid
+ // succession with increasing height values. Only the final value has any
+ // semblance of reality. I've encountered this when using the native
+ // password management integration
+ ios: greaterThan(keyboardHeight, max(prevKeyboardHeightValue, 0)),
+ // Android's keyboard can resize due to user interaction sometimes. In these
+ // cases it can get quite big, in which case we don't want to update
+ defaut: and(eq(prevKeyboardHeightValue, 0), greaterThan(keyboardHeight, 0)),
+ });
+ return block([
+ cond(
+ lessThan(prevKeyboardHeightValue, 0),
+ set(prevKeyboardHeightValue, keyboardHeight),
+ ),
+ cond(whenToUpdate, ratchetFunction),
+ set(prevKeyboardHeightValue, keyboardHeight),
+ ]);
+}
+
+export {
+ clamp,
+ delta,
+ gestureJustStarted,
+ gestureJustEnded,
+ runTiming,
+ ratchetAlongWithKeyboardHeight,
+};

File Metadata

Mime Type
text/x-diff
Expires
Mon, Dec 23, 10:38 AM (13 h, 7 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2690784
Default Alt Text
(44 KB)

Event Timeline