diff --git a/lib/types/account-types.js b/lib/types/account-types.js index ade3e08f2..c1915e8af 100644 --- a/lib/types/account-types.js +++ b/lib/types/account-types.js @@ -1,191 +1,192 @@ // @flow import type { PlatformDetails } from './device-types.js'; import type { CalendarQuery, CalendarResult, RawEntryInfo, } from './entry-types.js'; import type { RawMessageInfo, MessageTruncationStatuses, GenericMessagesResult, } from './message-types.js'; import type { PreRequestUserState } from './session-types.js'; import type { RawThreadInfo } from './thread-types.js'; import type { UserInfo, LoggedOutUserInfo, LoggedInUserInfo, OldLoggedInUserInfo, } from './user-types.js'; import type { PolicyType } from '../facts/policies.js'; import { values } from '../utils/objects.js'; export type ResetPasswordRequest = { +usernameOrEmail: string, }; export type LogOutResult = { +currentUserInfo: ?LoggedOutUserInfo, +preRequestUserState: PreRequestUserState, }; export type LogOutResponse = { +currentUserInfo: LoggedOutUserInfo, }; export type RegisterInfo = { ...LogInExtraInfo, +username: string, +password: string, +primaryIdentityPublicKey?: string, }; type DeviceTokenUpdateRequest = { +deviceToken: string, }; export type RegisterRequest = { +username: string, +password: string, +calendarQuery?: ?CalendarQuery, +deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, +platformDetails: PlatformDetails, +primaryIdentityPublicKey?: ?string, }; export type RegisterResponse = { id: string, rawMessageInfos: $ReadOnlyArray, currentUserInfo: OldLoggedInUserInfo | LoggedInUserInfo, cookieChange: { threadInfos: { +[id: string]: RawThreadInfo }, userInfos: $ReadOnlyArray, }, }; export type RegisterResult = { +currentUserInfo: LoggedInUserInfo, +rawMessageInfos: $ReadOnlyArray, +threadInfos: { +[id: string]: RawThreadInfo }, +userInfos: $ReadOnlyArray, +calendarQuery: CalendarQuery, }; export type DeleteAccountRequest = { +password: ?string, }; export const logInActionSources = Object.freeze({ cookieInvalidationResolutionAttempt: 'COOKIE_INVALIDATION_RESOLUTION_ATTEMPT', appStartCookieLoggedInButInvalidRedux: 'APP_START_COOKIE_LOGGED_IN_BUT_INVALID_REDUX', appStartReduxLoggedInButInvalidCookie: 'APP_START_REDUX_LOGGED_IN_BUT_INVALID_COOKIE', socketAuthErrorResolutionAttempt: 'SOCKET_AUTH_ERROR_RESOLUTION_ATTEMPT', sqliteOpFailure: 'SQLITE_OP_FAILURE', sqliteLoadFailure: 'SQLITE_LOAD_FAILURE', logInFromWebForm: 'LOG_IN_FROM_WEB_FORM', logInFromNativeForm: 'LOG_IN_FROM_NATIVE_FORM', logInFromNativeSIWE: 'LOG_IN_FROM_NATIVE_SIWE', corruptedDatabaseDeletion: 'CORRUPTED_DATABASE_DELETION', refetchUserDataAfterAcknowledgment: 'REFETCH_USER_DATA_AFTER_ACKNOWLEDGMENT', }); export type LogInActionSource = $Values; export type LogInStartingPayload = { +calendarQuery: CalendarQuery, +logInActionSource?: LogInActionSource, }; export type LogInExtraInfo = { +calendarQuery: CalendarQuery, +deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, + +primaryIdentityPublicKey?: string, }; export type LogInInfo = { ...LogInExtraInfo, +username: string, +password: string, +logInActionSource: LogInActionSource, +primaryIdentityPublicKey?: string, }; export type LogInRequest = { +usernameOrEmail?: ?string, +username?: ?string, +password: string, +calendarQuery?: ?CalendarQuery, +deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, +platformDetails: PlatformDetails, +watchedIDs: $ReadOnlyArray, +source?: LogInActionSource, }; export type LogInResponse = { +currentUserInfo: LoggedInUserInfo | OldLoggedInUserInfo, +rawMessageInfos: $ReadOnlyArray, +truncationStatuses: MessageTruncationStatuses, +userInfos: $ReadOnlyArray, +rawEntryInfos?: ?$ReadOnlyArray, +serverTime: number, +cookieChange: { +threadInfos: { +[id: string]: RawThreadInfo }, +userInfos: $ReadOnlyArray, }, +notAcknowledgedPolicies?: $ReadOnlyArray, }; export type LogInResult = { +threadInfos: { +[id: string]: RawThreadInfo }, +currentUserInfo: LoggedInUserInfo, +messagesResult: GenericMessagesResult, +userInfos: $ReadOnlyArray, +calendarResult: CalendarResult, +updatesCurrentAsOf: number, +logInActionSource: LogInActionSource, +notAcknowledgedPolicies?: $ReadOnlyArray, }; export type UpdatePasswordRequest = { code: string, password: string, calendarQuery?: ?CalendarQuery, deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, platformDetails: PlatformDetails, watchedIDs: $ReadOnlyArray, }; export type PolicyAcknowledgmentRequest = { +policy: PolicyType, }; export type EmailSubscriptionRequest = { +email: string, }; export type UpdateUserSettingsRequest = { +name: 'default_user_notifications', +data: NotificationTypes, }; export const userSettingsTypes = Object.freeze({ DEFAULT_NOTIFICATIONS: 'default_user_notifications', }); export type DefaultNotificationPayload = { +default_user_notifications: ?NotificationTypes, }; export const notificationTypes = Object.freeze({ FOCUSED: 'focused', BADGE_ONLY: 'badge_only', BACKGROUND: 'background', }); export type NotificationTypes = $Values; export const notificationTypeValues: $ReadOnlyArray = values( notificationTypes, ); diff --git a/native/account/log-in-panel.react.js b/native/account/log-in-panel.react.js index f8832602f..588596097 100644 --- a/native/account/log-in-panel.react.js +++ b/native/account/log-in-panel.react.js @@ -1,406 +1,406 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View, StyleSheet, Alert, Keyboard, Platform } from 'react-native'; import Animated from 'react-native-reanimated'; import { logInActionTypes, logIn } from 'lib/actions/user-actions.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { validEmailRegex, oldValidUsernameRegex, } from 'lib/shared/account-utils.js'; import { type LogInInfo, type LogInExtraInfo, type LogInResult, type LogInStartingPayload, logInActionSources, } from 'lib/types/account-types.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import { useServerCall, useDispatchActionPromise, type DispatchActionPromise, } from 'lib/utils/action-utils.js'; import { TextInput } from './modal-components.react.js'; import { fetchNativeCredentials, setNativeCredentials, } from './native-credentials.js'; import { PanelButton, Panel } from './panel-components.react.js'; import PasswordInput from './password-input.react.js'; import SWMansionIcon from '../components/swmansion-icon.react.js'; import { commCoreModule } from '../native-modules.js'; import { NavContext } from '../navigation/navigation-context.js'; import { useSelector } from '../redux/redux-utils.js'; import { nativeLogInExtraInfoSelector } from '../selectors/account-selectors.js'; import type { KeyPressEvent } from '../types/react-native.js'; import type { StateContainer } from '../utils/state-container.js'; export type LogInState = { +usernameInputText: ?string, +passwordInputText: ?string, }; type BaseProps = { +setActiveAlert: (activeAlert: boolean) => void, +opacityValue: Animated.Node, +logInState: StateContainer, }; type Props = { ...BaseProps, +loadingStatus: LoadingStatus, - +logInExtraInfo: () => LogInExtraInfo, + +logInExtraInfo: () => Promise, +dispatchActionPromise: DispatchActionPromise, +logIn: (logInInfo: LogInInfo) => Promise, +primaryIdentityPublicKey: ?string, }; class LogInPanel extends React.PureComponent { usernameInput: ?TextInput; passwordInput: ?PasswordInput; componentDidMount() { this.attemptToFetchCredentials(); } get usernameInputText(): string { return this.props.logInState.state.usernameInputText || ''; } get passwordInputText(): string { return this.props.logInState.state.passwordInputText || ''; } async attemptToFetchCredentials() { if ( this.props.logInState.state.usernameInputText !== null && this.props.logInState.state.usernameInputText !== undefined ) { return; } const credentials = await fetchNativeCredentials(); if (!credentials) { return; } if ( this.props.logInState.state.usernameInputText !== null && this.props.logInState.state.usernameInputText !== undefined ) { return; } this.props.logInState.setState({ usernameInputText: credentials.username, passwordInputText: credentials.password, }); } render(): React.Node { return ( ); } usernameInputRef: (usernameInput: ?TextInput) => void = usernameInput => { this.usernameInput = usernameInput; if (Platform.OS === 'ios' && usernameInput) { setTimeout(() => usernameInput.focus()); } }; focusUsernameInput: () => void = () => { invariant(this.usernameInput, 'ref should be set'); this.usernameInput.focus(); }; passwordInputRef: (passwordInput: ?PasswordInput) => void = passwordInput => { this.passwordInput = passwordInput; }; focusPasswordInput: () => void = () => { invariant(this.passwordInput, 'ref should be set'); this.passwordInput.focus(); }; onChangeUsernameInputText: (text: string) => void = text => { this.props.logInState.setState({ usernameInputText: text.trim() }); }; onUsernameKeyPress: (event: KeyPressEvent) => void = event => { const { key } = event.nativeEvent; if ( key.length > 1 && key !== 'Backspace' && key !== 'Enter' && this.passwordInputText.length === 0 ) { this.focusPasswordInput(); } }; onChangePasswordInputText: (text: string) => void = text => { this.props.logInState.setState({ passwordInputText: text }); }; - onSubmit: () => void = () => { + onSubmit: () => Promise = async () => { this.props.setActiveAlert(true); if (this.usernameInputText.search(validEmailRegex) > -1) { Alert.alert( 'Can’t log in with email', 'You need to log in with your username now', [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }], { cancelable: false }, ); return; } else if (this.usernameInputText.search(oldValidUsernameRegex) === -1) { Alert.alert( 'Invalid username', 'Alphanumeric usernames only', [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }], { cancelable: false }, ); return; } else if (this.passwordInputText === '') { Alert.alert( 'Empty password', 'Password cannot be empty', [{ text: 'OK', onPress: this.onPasswordAlertAcknowledged }], { cancelable: false }, ); return; } Keyboard.dismiss(); - const extraInfo = this.props.logInExtraInfo(); + const extraInfo = await this.props.logInExtraInfo(); this.props.dispatchActionPromise( logInActionTypes, this.logInAction(extraInfo), undefined, ({ calendarQuery: extraInfo.calendarQuery }: LogInStartingPayload), ); }; async logInAction(extraInfo: LogInExtraInfo): Promise { const { primaryIdentityPublicKey } = this.props; try { invariant( primaryIdentityPublicKey !== null && primaryIdentityPublicKey !== undefined, 'primaryIdentityPublicKey must exist in logInAction', ); const result = await this.props.logIn({ ...extraInfo, username: this.usernameInputText, password: this.passwordInputText, logInActionSource: logInActionSources.logInFromNativeForm, primaryIdentityPublicKey: primaryIdentityPublicKey, }); this.props.setActiveAlert(false); await setNativeCredentials({ username: result.currentUserInfo.username, password: this.passwordInputText, }); return result; } catch (e) { if (e.message === 'invalid_credentials') { Alert.alert( 'Incorrect username or password', "Either that user doesn't exist, or the password is incorrect", [{ text: 'OK', onPress: this.onUnsuccessfulLoginAlertAckowledged }], { cancelable: false }, ); } else if (e.message === 'client_version_unsupported') { const app = Platform.select({ ios: 'App Store', android: 'Play Store', }); Alert.alert( 'App out of date', 'Your app version is pretty old, and the server doesn’t know how ' + `to speak to it anymore. Please use the ${app} app to update!`, [{ text: 'OK', onPress: this.onAppOutOfDateAlertAcknowledged }], { cancelable: false }, ); } else { Alert.alert( 'Unknown error', 'Uhh... try again?', [{ text: 'OK', onPress: this.onUnknownErrorAlertAcknowledged }], { cancelable: false }, ); } throw e; } } onUnsuccessfulLoginAlertAckowledged: () => void = () => { this.props.setActiveAlert(false); this.props.logInState.setState( { usernameInputText: '', passwordInputText: '', }, this.focusUsernameInput, ); }; onUsernameAlertAcknowledged: () => void = () => { this.props.setActiveAlert(false); this.props.logInState.setState( { usernameInputText: '', }, this.focusUsernameInput, ); }; onPasswordAlertAcknowledged: () => void = () => { this.props.setActiveAlert(false); this.props.logInState.setState( { passwordInputText: '', }, this.focusPasswordInput, ); }; onUnknownErrorAlertAcknowledged: () => void = () => { this.props.setActiveAlert(false); this.props.logInState.setState( { usernameInputText: '', passwordInputText: '', }, this.focusUsernameInput, ); }; onAppOutOfDateAlertAcknowledged: () => void = () => { this.props.setActiveAlert(false); }; } export type InnerLogInPanel = LogInPanel; const styles = StyleSheet.create({ footer: { flexDirection: 'row', justifyContent: 'flex-end', }, icon: { bottom: 10, left: 4, position: 'absolute', }, input: { paddingLeft: 35, }, row: { marginHorizontal: 24, }, }); const loadingStatusSelector = createLoadingStatusSelector(logInActionTypes); const ConnectedLogInPanel: React.ComponentType = React.memo( function ConnectedLogInPanel(props: BaseProps) { const loadingStatus = useSelector(loadingStatusSelector); const navContext = React.useContext(NavContext); const logInExtraInfo = useSelector(state => nativeLogInExtraInfoSelector({ redux: state, navContext, }), ); const dispatchActionPromise = useDispatchActionPromise(); const callLogIn = useServerCall(logIn); const [ primaryIdentityPublicKey, setPrimaryIdentityPublicKey, ] = React.useState(null); React.useEffect(() => { (async () => { await commCoreModule.initializeCryptoAccount('PLACEHOLDER'); const { ed25519 } = await commCoreModule.getUserPublicKey(); setPrimaryIdentityPublicKey(ed25519); })(); }, []); return ( ); }, ); export default ConnectedLogInPanel; diff --git a/native/account/panel-components.react.js b/native/account/panel-components.react.js index b54f53271..a9072a904 100644 --- a/native/account/panel-components.react.js +++ b/native/account/panel-components.react.js @@ -1,128 +1,128 @@ // @flow import Icon from '@expo/vector-icons/FontAwesome.js'; import * as React from 'react'; import { View, ActivityIndicator, Text, StyleSheet, ScrollView, } from 'react-native'; import Animated from 'react-native-reanimated'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import Button from '../components/button.react.js'; import { useSelector } from '../redux/redux-utils.js'; import type { ViewStyle } from '../types/styles.js'; type ButtonProps = { +text: string, +loadingStatus: LoadingStatus, - +onSubmit: () => void, + +onSubmit: () => mixed, +disabled?: boolean, }; function PanelButton(props: ButtonProps): React.Node { let buttonIcon; if (props.loadingStatus === 'loading') { buttonIcon = ( ); } else { buttonIcon = ( ); } return ( ); } type PanelProps = { +opacityValue: Animated.Node, +children: React.Node, +style?: ViewStyle, }; function Panel(props: PanelProps): React.Node { const dimensions = useSelector(state => state.dimensions); const containerStyle = React.useMemo( () => [ styles.container, { opacity: props.opacityValue, marginTop: dimensions.height < 641 ? 15 : 40, }, props.style, ], [props.opacityValue, props.style, dimensions.height], ); return ( {props.children} ); } const styles = StyleSheet.create({ container: { backgroundColor: '#FFFFFFAA', borderRadius: 6, marginLeft: 20, marginRight: 20, paddingTop: 6, }, innerSubmitButton: { alignItems: 'flex-end', flexDirection: 'row', paddingVertical: 6, }, loadingIndicatorContainer: { paddingBottom: 2, width: 14, }, submitButton: { borderBottomRightRadius: 6, justifyContent: 'center', paddingLeft: 10, paddingRight: 18, }, submitButtonHorizontalContainer: { alignSelf: 'flex-end', }, submitButtonVerticalContainer: { flexGrow: 1, justifyContent: 'center', }, submitContentIconContainer: { paddingBottom: 5, width: 14, }, submitContentText: { color: '#555', fontFamily: 'OpenSans-Semibold', fontSize: 18, paddingRight: 7, }, }); export { PanelButton, Panel }; diff --git a/native/account/register-panel.react.js b/native/account/register-panel.react.js index 415990b39..fdeac3bd0 100644 --- a/native/account/register-panel.react.js +++ b/native/account/register-panel.react.js @@ -1,499 +1,499 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { Text, View, StyleSheet, Platform, Keyboard, Alert, Linking, } from 'react-native'; import Animated from 'react-native-reanimated'; import { registerActionTypes, register } from 'lib/actions/user-actions.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { validUsernameRegex } from 'lib/shared/account-utils.js'; import type { RegisterInfo, LogInExtraInfo, RegisterResult, LogInStartingPayload, } from 'lib/types/account-types.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import { useServerCall, useDispatchActionPromise, type DispatchActionPromise, } from 'lib/utils/action-utils.js'; import { TextInput } from './modal-components.react.js'; import { setNativeCredentials } from './native-credentials.js'; import { PanelButton, Panel } from './panel-components.react.js'; import SWMansionIcon from '../components/swmansion-icon.react.js'; import { commCoreModule } from '../native-modules.js'; import { NavContext } from '../navigation/navigation-context.js'; import { useSelector } from '../redux/redux-utils.js'; import { nativeLogInExtraInfoSelector } from '../selectors/account-selectors.js'; import type { KeyPressEvent } from '../types/react-native.js'; import { type StateContainer } from '../utils/state-container.js'; export type RegisterState = { +usernameInputText: string, +passwordInputText: string, +confirmPasswordInputText: string, }; type BaseProps = { +setActiveAlert: (activeAlert: boolean) => void, +opacityValue: Animated.Node, +registerState: StateContainer, }; type Props = { ...BaseProps, +loadingStatus: LoadingStatus, - +logInExtraInfo: () => LogInExtraInfo, + +logInExtraInfo: () => Promise, +dispatchActionPromise: DispatchActionPromise, +register: (registerInfo: RegisterInfo) => Promise, +primaryIdentityPublicKey: ?string, }; type State = { +confirmPasswordFocused: boolean, }; class RegisterPanel extends React.PureComponent { state: State = { confirmPasswordFocused: false, }; usernameInput: ?TextInput; passwordInput: ?TextInput; confirmPasswordInput: ?TextInput; passwordBeingAutoFilled = false; render() { let confirmPasswordTextInputExtraProps; if ( Platform.OS !== 'ios' || this.state.confirmPasswordFocused || this.props.registerState.state.confirmPasswordInputText.length > 0 ) { confirmPasswordTextInputExtraProps = { secureTextEntry: true, textContentType: 'password', }; } let onPasswordKeyPress; if (Platform.OS === 'ios') { onPasswordKeyPress = this.onPasswordKeyPress; } /* eslint-disable react-native/no-raw-text */ const privatePolicyNotice = ( By signing up, you agree to our{' '} Terms {' & '} Privacy Policy . ); /* eslint-enable react-native/no-raw-text */ return ( {privatePolicyNotice} ); } usernameInputRef = (usernameInput: ?TextInput) => { this.usernameInput = usernameInput; }; passwordInputRef = (passwordInput: ?TextInput) => { this.passwordInput = passwordInput; }; confirmPasswordInputRef = (confirmPasswordInput: ?TextInput) => { this.confirmPasswordInput = confirmPasswordInput; }; focusUsernameInput = () => { invariant(this.usernameInput, 'ref should be set'); this.usernameInput.focus(); }; focusPasswordInput = () => { invariant(this.passwordInput, 'ref should be set'); this.passwordInput.focus(); }; focusConfirmPasswordInput = () => { invariant(this.confirmPasswordInput, 'ref should be set'); this.confirmPasswordInput.focus(); }; onTermsOfUsePressed = () => { Linking.openURL('https://comm.app/terms'); }; onPrivacyPolicyPressed = () => { Linking.openURL('https://comm.app/privacy'); }; onChangeUsernameInputText = (text: string) => { this.props.registerState.setState({ usernameInputText: text }); }; onChangePasswordInputText = (text: string) => { const stateUpdate = {}; stateUpdate.passwordInputText = text; if (this.passwordBeingAutoFilled) { this.passwordBeingAutoFilled = false; stateUpdate.confirmPasswordInputText = text; } this.props.registerState.setState(stateUpdate); }; onPasswordKeyPress = (event: KeyPressEvent) => { const { key } = event.nativeEvent; if ( key.length > 1 && key !== 'Backspace' && key !== 'Enter' && this.props.registerState.state.confirmPasswordInputText.length === 0 ) { this.passwordBeingAutoFilled = true; } }; onChangeConfirmPasswordInputText = (text: string) => { this.props.registerState.setState({ confirmPasswordInputText: text }); }; onConfirmPasswordFocus = () => { this.setState({ confirmPasswordFocused: true }); }; - onSubmit = () => { + onSubmit = async () => { this.props.setActiveAlert(true); if (this.props.registerState.state.passwordInputText === '') { Alert.alert( 'Empty password', 'Password cannot be empty', [{ text: 'OK', onPress: this.onPasswordAlertAcknowledged }], { cancelable: false }, ); } else if ( this.props.registerState.state.passwordInputText !== this.props.registerState.state.confirmPasswordInputText ) { Alert.alert( 'Passwords don’t match', 'Password fields must contain the same password', [{ text: 'OK', onPress: this.onPasswordAlertAcknowledged }], { cancelable: false }, ); } else if ( this.props.registerState.state.usernameInputText.search( validUsernameRegex, ) === -1 ) { Alert.alert( 'Invalid username', 'Usernames must be at least six characters long, start with either a ' + 'letter or a number, and may contain only letters, numbers, or the ' + 'characters “-” and “_”', [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }], { cancelable: false }, ); } else { Keyboard.dismiss(); - const extraInfo = this.props.logInExtraInfo(); + const extraInfo = await this.props.logInExtraInfo(); this.props.dispatchActionPromise( registerActionTypes, this.registerAction(extraInfo), undefined, ({ calendarQuery: extraInfo.calendarQuery }: LogInStartingPayload), ); } }; onPasswordAlertAcknowledged = () => { this.props.setActiveAlert(false); this.props.registerState.setState( { passwordInputText: '', confirmPasswordInputText: '', }, () => { invariant(this.passwordInput, 'ref should exist'); this.passwordInput.focus(); }, ); }; onUsernameAlertAcknowledged = () => { this.props.setActiveAlert(false); this.props.registerState.setState( { usernameInputText: '', }, () => { invariant(this.usernameInput, 'ref should exist'); this.usernameInput.focus(); }, ); }; async registerAction(extraInfo: LogInExtraInfo) { const { primaryIdentityPublicKey } = this.props; try { invariant( primaryIdentityPublicKey !== null && primaryIdentityPublicKey !== undefined, 'primaryIdentityPublicKey must exist in logInAction', ); const result = await this.props.register({ ...extraInfo, username: this.props.registerState.state.usernameInputText, password: this.props.registerState.state.passwordInputText, primaryIdentityPublicKey: primaryIdentityPublicKey, }); this.props.setActiveAlert(false); await setNativeCredentials({ username: result.currentUserInfo.username, password: this.props.registerState.state.passwordInputText, }); return result; } catch (e) { if (e.message === 'username_reserved') { Alert.alert( 'Username reserved', 'This username is currently reserved. Please contact support@' + 'comm.app if you would like to claim this account.', [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }], { cancelable: false }, ); } else if (e.message === 'username_taken') { Alert.alert( 'Username taken', 'An account with that username already exists', [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }], { cancelable: false }, ); } else if (e.message === 'client_version_unsupported') { const app = Platform.select({ ios: 'App Store', android: 'Play Store', }); Alert.alert( 'App out of date', 'Your app version is pretty old, and the server doesn’t know how ' + `to speak to it anymore. Please use the ${app} app to update!`, [{ text: 'OK', onPress: this.onAppOutOfDateAlertAcknowledged }], { cancelable: false }, ); } else { Alert.alert( 'Unknown error', 'Uhh... try again?', [{ text: 'OK', onPress: this.onUnknownErrorAlertAcknowledged }], { cancelable: false }, ); } throw e; } } onUnknownErrorAlertAcknowledged = () => { this.props.setActiveAlert(false); this.props.registerState.setState( { usernameInputText: '', passwordInputText: '', confirmPasswordInputText: '', }, () => { invariant(this.usernameInput, 'ref should exist'); this.usernameInput.focus(); }, ); }; onAppOutOfDateAlertAcknowledged = () => { this.props.setActiveAlert(false); }; } const styles = StyleSheet.create({ container: { zIndex: 2, }, footer: { alignItems: 'stretch', flexDirection: 'row', flexShrink: 1, justifyContent: 'space-between', paddingLeft: 24, }, hyperlinkText: { color: '#036AFF', fontWeight: 'bold', }, icon: { bottom: 10, left: 4, position: 'absolute', }, input: { paddingLeft: 35, }, notice: { alignSelf: 'center', display: 'flex', flexShrink: 1, maxWidth: 190, paddingBottom: 18, paddingRight: 8, paddingTop: 12, }, noticeText: { color: '#444', fontSize: 13, lineHeight: 20, textAlign: 'center', }, row: { marginHorizontal: 24, }, }); const loadingStatusSelector = createLoadingStatusSelector(registerActionTypes); const ConnectedRegisterPanel: React.ComponentType = React.memo( function ConnectedRegisterPanel(props: BaseProps) { const loadingStatus = useSelector(loadingStatusSelector); const navContext = React.useContext(NavContext); const logInExtraInfo = useSelector(state => nativeLogInExtraInfoSelector({ redux: state, navContext, }), ); const dispatchActionPromise = useDispatchActionPromise(); const callRegister = useServerCall(register); const [ primaryIdentityPublicKey, setPrimaryIdentityPublicKey, ] = React.useState(null); React.useEffect(() => { (async () => { await commCoreModule.initializeCryptoAccount('PLACEHOLDER'); const { ed25519 } = await commCoreModule.getUserPublicKey(); setPrimaryIdentityPublicKey(ed25519); })(); }, []); return ( ); }, ); export default ConnectedRegisterPanel; diff --git a/native/account/resolve-invalidated-cookie.js b/native/account/resolve-invalidated-cookie.js index f4298ab87..ce9edc1e1 100644 --- a/native/account/resolve-invalidated-cookie.js +++ b/native/account/resolve-invalidated-cookie.js @@ -1,38 +1,38 @@ // @flow import { logInActionTypes, logIn } from 'lib/actions/user-actions.js'; import type { LogInActionSource } from 'lib/types/account-types.js'; import type { DispatchRecoveryAttempt } from 'lib/utils/action-utils.js'; import type { CallServerEndpoint } from 'lib/utils/call-server-endpoint.js'; import { fetchNativeKeychainCredentials } from './native-credentials.js'; import { getGlobalNavContext } from '../navigation/icky-global.js'; import { store } from '../redux/redux-setup.js'; import { nativeLogInExtraInfoSelector } from '../selectors/account-selectors.js'; async function resolveInvalidatedCookie( callServerEndpoint: CallServerEndpoint, dispatchRecoveryAttempt: DispatchRecoveryAttempt, logInActionSource: LogInActionSource, ) { const keychainCredentials = await fetchNativeKeychainCredentials(); if (!keychainCredentials) { return; } - const extraInfo = nativeLogInExtraInfoSelector({ + const extraInfo = await nativeLogInExtraInfoSelector({ redux: store.getState(), navContext: getGlobalNavContext(), })(); const { calendarQuery } = extraInfo; await dispatchRecoveryAttempt( logInActionTypes, logIn(callServerEndpoint)({ ...keychainCredentials, ...extraInfo, logInActionSource, }), { calendarQuery }, ); } export { resolveInvalidatedCookie }; diff --git a/native/account/siwe-panel.react.js b/native/account/siwe-panel.react.js index eddcaf3ff..bfe371332 100644 --- a/native/account/siwe-panel.react.js +++ b/native/account/siwe-panel.react.js @@ -1,268 +1,268 @@ // @flow import BottomSheet from '@gorhom/bottom-sheet'; import * as React from 'react'; import { ActivityIndicator, View, Alert } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import WebView from 'react-native-webview'; import { getSIWENonce, getSIWENonceActionTypes, siweAuth, siweAuthActionTypes, } from 'lib/actions/siwe-actions.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import type { LogInStartingPayload } from 'lib/types/account-types.js'; import type { SIWEWebViewMessage } from 'lib/types/siwe-types.js'; import { useServerCall, useDispatchActionPromise, } from 'lib/utils/action-utils.js'; import type { LoggedOutMode } from './logged-out-modal.react.js'; import { commCoreModule } from '../native-modules.js'; import { NavContext } from '../navigation/navigation-context.js'; import { useSelector } from '../redux/redux-utils.js'; import { nativeLogInExtraInfoSelector } from '../selectors/account-selectors.js'; import { defaultLandingURLPrefix } from '../utils/url-utils.js'; const commSIWE = `${defaultLandingURLPrefix}/siwe`; const getSIWENonceLoadingStatusSelector = createLoadingStatusSelector( getSIWENonceActionTypes, ); const siweAuthLoadingStatusSelector = createLoadingStatusSelector( siweAuthActionTypes, ); type Props = { +onClose: () => mixed, +nextMode: LoggedOutMode, }; function SIWEPanel(props: Props): React.Node { const navContext = React.useContext(NavContext); const dispatchActionPromise = useDispatchActionPromise(); const getSIWENonceCall = useServerCall(getSIWENonce); const siweAuthCall = useServerCall(siweAuth); const logInExtraInfo = useSelector(state => nativeLogInExtraInfoSelector({ redux: state, navContext, }), ); const getSIWENonceCallFailed = useSelector( state => getSIWENonceLoadingStatusSelector(state) === 'error', ); const { onClose } = props; React.useEffect(() => { if (getSIWENonceCallFailed) { Alert.alert( 'Unknown error', 'Uhh... try again?', [{ text: 'OK', onPress: onClose }], { cancelable: false }, ); } }, [getSIWENonceCallFailed, onClose]); const siweAuthCallLoading = useSelector( state => siweAuthLoadingStatusSelector(state) === 'loading', ); const [nonce, setNonce] = React.useState(null); const [ primaryIdentityPublicKey, setPrimaryIdentityPublicKey, ] = React.useState(null); React.useEffect(() => { (async () => { dispatchActionPromise( getSIWENonceActionTypes, (async () => { const response = await getSIWENonceCall(); setNonce(response); })(), ); await commCoreModule.initializeCryptoAccount('PLACEHOLDER'); const { ed25519 } = await commCoreModule.getUserPublicKey(); setPrimaryIdentityPublicKey(ed25519); })(); }, [dispatchActionPromise, getSIWENonceCall]); const [isLoading, setLoading] = React.useState(true); const [isWalletConnectModalOpen, setWalletConnectModalOpen] = React.useState( false, ); const insets = useSafeAreaInsets(); const bottomInset = insets.bottom; const snapPoints = React.useMemo(() => { if (isLoading) { return [1]; } else if (isWalletConnectModalOpen) { return [bottomInset + 600]; } else { return [bottomInset + 435, bottomInset + 600]; } }, [isLoading, isWalletConnectModalOpen, bottomInset]); const bottomSheetRef = React.useRef(); const snapToIndex = bottomSheetRef.current?.snapToIndex; React.useEffect(() => { // When the snapPoints change, always reset to the first one // Without this, when we close the WalletConnect modal we don't resize snapToIndex?.(0); }, [snapToIndex, snapPoints]); const callSIWE = React.useCallback( async (message, signature, extraInfo) => { try { return await siweAuthCall({ message, signature, ...extraInfo, }); } catch (e) { Alert.alert( 'Unknown error', 'Uhh... try again?', [{ text: 'OK', onPress: onClose }], { cancelable: false }, ); throw e; } }, [onClose, siweAuthCall], ); const handleSIWE = React.useCallback( - ({ message, signature }) => { - const extraInfo = logInExtraInfo(); + async ({ message, signature }) => { + const extraInfo = await logInExtraInfo(); dispatchActionPromise( siweAuthActionTypes, callSIWE(message, signature, extraInfo), undefined, ({ calendarQuery: extraInfo.calendarQuery }: LogInStartingPayload), ); }, [logInExtraInfo, dispatchActionPromise, callSIWE], ); const closeBottomSheet = bottomSheetRef.current?.close; const { nextMode } = props; const disableOnClose = React.useRef(false); const handleMessage = React.useCallback( - event => { + async event => { const data: SIWEWebViewMessage = JSON.parse(event.nativeEvent.data); if (data.type === 'siwe_success') { const { address, message, signature } = data; if (address && signature) { disableOnClose.current = true; closeBottomSheet?.(); - handleSIWE({ message, signature }); + await handleSIWE({ message, signature }); } } else if (data.type === 'siwe_closed') { onClose(); closeBottomSheet?.(); } else if (data.type === 'walletconnect_modal_update') { setWalletConnectModalOpen(data.state === 'open'); } }, [handleSIWE, onClose, closeBottomSheet], ); const prevNextModeRef = React.useRef(); React.useEffect(() => { if (nextMode === 'prompt' && prevNextModeRef.current === 'siwe') { closeBottomSheet?.(); } prevNextModeRef.current = nextMode; }, [nextMode, closeBottomSheet]); const source = React.useMemo( () => ({ uri: commSIWE, headers: { 'siwe-nonce': nonce, 'siwe-primary-identity-public-key': primaryIdentityPublicKey, }, }), [nonce, primaryIdentityPublicKey], ); const onWebViewLoaded = React.useCallback(() => { setLoading(false); }, []); const backgroundStyle = React.useMemo( () => ({ backgroundColor: '#242529', }), [], ); const bottomSheetHandleIndicatorStyle = React.useMemo( () => ({ backgroundColor: 'white', }), [], ); const onBottomSheetChange = React.useCallback( (index: number) => { if (disableOnClose.current) { disableOnClose.current = false; return; } if (index === -1) { onClose(); } }, [onClose], ); let bottomSheet; if (nonce && primaryIdentityPublicKey) { bottomSheet = ( ); } let activity; if (!getSIWENonceCallFailed && (isLoading || siweAuthCallLoading)) { activity = ; } const activityContainer = React.useMemo( () => ({ flex: 1, }), [], ); return ( <> {activity} {bottomSheet} ); } export default SIWEPanel; diff --git a/native/selectors/account-selectors.js b/native/selectors/account-selectors.js index d9757ac80..3419563ba 100644 --- a/native/selectors/account-selectors.js +++ b/native/selectors/account-selectors.js @@ -1,45 +1,56 @@ // @flow import { createSelector } from 'reselect'; import { logInExtraInfoSelector } from 'lib/selectors/account-selectors.js'; import type { LogInExtraInfo } from 'lib/types/account-types.js'; import type { UserPolicies } from 'lib/types/policy-types.js'; import { values } from 'lib/utils/objects.js'; +import { commCoreModule } from '../native-modules.js'; import { calendarActiveSelector } from '../navigation/nav-selectors.js'; import type { AppState } from '../redux/state-types.js'; import type { ConnectivityInfo } from '../types/connectivity.js'; import type { NavPlusRedux } from '../types/selector-types.js'; const nativeLogInExtraInfoSelector: ( input: NavPlusRedux, -) => () => LogInExtraInfo = createSelector( +) => () => Promise = createSelector( (input: NavPlusRedux) => logInExtraInfoSelector(input.redux), (input: NavPlusRedux) => calendarActiveSelector(input.navContext), ( logInExtraInfoFunc: (calendarActive: boolean) => LogInExtraInfo, calendarActive: boolean, - ) => () => logInExtraInfoFunc(calendarActive), + ) => { + const loginExtraFuncWithIdentityKey = async () => { + await commCoreModule.initializeCryptoAccount('PLACEHOLDER'); + const { ed25519 } = await commCoreModule.getUserPublicKey(); + return { + ...logInExtraInfoFunc(calendarActive), + primaryIdentityPublicKey: ed25519, + }; + }; + return loginExtraFuncWithIdentityKey; + }, ); const noDataAfterPolicyAcknowledgmentSelector: ( state: AppState, ) => boolean = createSelector( (state: AppState) => state.connectivity, (state: AppState) => state.messageStore.currentAsOf, (state: AppState) => state.userPolicies, ( connectivity: ConnectivityInfo, currentAsOf: number, userPolicies: UserPolicies, ) => connectivity.connected && currentAsOf === 0 && values(userPolicies).every(policy => policy.isAcknowledged), ); export { nativeLogInExtraInfoSelector, noDataAfterPolicyAcknowledgmentSelector, };