Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3510027
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
44 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Dec 23, 10:38 AM (17 h, 53 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2690784
Default Alt Text
(44 KB)
Attached To
Mode
rCOMM Comm
Attached
Detach File
Event Timeline
Log In to Comment