diff --git a/lib/actions/siwe-actions.js b/lib/actions/siwe-actions.js index d155415c3..2f1f9d92c 100644 --- a/lib/actions/siwe-actions.js +++ b/lib/actions/siwe-actions.js @@ -1,96 +1,96 @@ // @flow import { mergeUserInfos } from './user-actions.js'; import type { CallSingleKeyserverEndpoint, CallSingleKeyserverEndpointOptions, } from '../keyserver-conn/call-single-keyserver-endpoint.js'; import threadWatcher from '../shared/thread-watcher.js'; import { permissionsAndAuthRelatedRequestTimeout } from '../shared/timeouts.js'; import { type LegacyLogInResult, logInActionSources, } from '../types/account-types.js'; -import type { SIWEAuthServerCall } from '../types/siwe-types.js'; +import type { LegacySIWEAuthServerCall } from '../types/siwe-types.js'; import { authoritativeKeyserverID } from '../utils/authoritative-keyserver.js'; import { getConfig } from '../utils/config.js'; const getSIWENonceActionTypes = Object.freeze({ started: 'GET_SIWE_NONCE_STARTED', success: 'GET_SIWE_NONCE_SUCCESS', failed: 'GET_SIWE_NONCE_FAILED', }); const getSIWENonce = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): (() => Promise) => async () => { const response = await callSingleKeyserverEndpoint('siwe_nonce'); return response.nonce; }; const legacySiweAuthActionTypes = Object.freeze({ started: 'LEGACY_SIWE_AUTH_STARTED', success: 'LEGACY_SIWE_AUTH_SUCCESS', failed: 'LEGACY_SIWE_AUTH_FAILED', }); const legacySiweAuthCallSingleKeyserverEndpointOptions = { timeout: permissionsAndAuthRelatedRequestTimeout, }; const legacySiweAuth = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): (( - siweAuthPayload: SIWEAuthServerCall, + siweAuthPayload: LegacySIWEAuthServerCall, options?: ?CallSingleKeyserverEndpointOptions, ) => Promise) => async (siweAuthPayload, options) => { const watchedIDs = threadWatcher.getWatchedIDs(); const deviceTokenUpdateRequest = siweAuthPayload.deviceTokenUpdateRequest[authoritativeKeyserverID()]; const { preRequestUserInfo, ...rest } = siweAuthPayload; const response = await callSingleKeyserverEndpoint( 'siwe_auth', { ...rest, watchedIDs, deviceTokenUpdateRequest, platformDetails: getConfig().platformDetails, }, { ...legacySiweAuthCallSingleKeyserverEndpointOptions, ...options, }, ); const userInfos = mergeUserInfos( response.userInfos, response.cookieChange.userInfos, ); return { threadInfos: response.cookieChange.threadInfos, currentUserInfo: response.currentUserInfo, calendarResult: { calendarQuery: siweAuthPayload.calendarQuery, rawEntryInfos: response.rawEntryInfos, }, messagesResult: { messageInfos: response.rawMessageInfos, truncationStatus: response.truncationStatuses, watchedIDsAtRequestTime: watchedIDs, currentAsOf: { [authoritativeKeyserverID()]: response.serverTime }, }, userInfos, updatesCurrentAsOf: { [authoritativeKeyserverID()]: response.serverTime }, authActionSource: logInActionSources.logInFromNativeSIWE, notAcknowledgedPolicies: response.notAcknowledgedPolicies, preRequestUserInfo: null, }; }; export { getSIWENonceActionTypes, getSIWENonce, legacySiweAuthActionTypes, legacySiweAuth, }; diff --git a/lib/selectors/account-selectors.js b/lib/selectors/account-selectors.js index d5f60bec5..9fbea3c3a 100644 --- a/lib/selectors/account-selectors.js +++ b/lib/selectors/account-selectors.js @@ -1,115 +1,115 @@ // @flow import _memoize from 'lodash/memoize.js'; import * as React from 'react'; import { createSelector } from 'reselect'; import { cookieSelector, sessionIDSelector, deviceTokensSelector, } from './keyserver-selectors.js'; import { useDerivedObject } from '../hooks/objects.js'; import type { - LogInExtraInfo, + LegacyLogInExtraInfo, DeviceTokenUpdateRequest, } from '../types/account-types.js'; import type { CalendarFilter } from '../types/filter-types.js'; import type { KeyserverInfo } from '../types/keyserver-types.js'; import type { BaseNavInfo } from '../types/nav-types.js'; import type { AppState, BaseAppState } from '../types/redux-types.js'; import type { PreRequestUserState, PreRequestUserKeyserverSessionInfo, } from '../types/session-types.js'; import type { CurrentUserInfo } from '../types/user-types.js'; import { useSelector } from '../utils/redux-utils.js'; -const logInExtraInfoSelector: (state: AppState) => LogInExtraInfo = +const legacyLogInExtraInfoSelector: (state: AppState) => LegacyLogInExtraInfo = createSelector( (state: BaseAppState<>) => state.navInfo, (state: BaseAppState<>) => state.calendarFilters, (state: BaseAppState<>) => state.currentUserInfo, deviceTokensSelector, ( navInfo: BaseNavInfo, calendarFilters: $ReadOnlyArray, currentUserInfo: ?CurrentUserInfo, deviceTokens: { +[keyserverID: string]: ?string }, ) => { const deviceTokenUpdateRequest: { [string]: DeviceTokenUpdateRequest } = {}; for (const keyserverID in deviceTokens) { if (deviceTokens[keyserverID]) { deviceTokenUpdateRequest[keyserverID] = { deviceToken: deviceTokens[keyserverID], }; } } return { calendarQuery: { startDate: navInfo.startDate, endDate: navInfo.endDate, filters: calendarFilters, }, deviceTokenUpdateRequest, preRequestUserInfo: currentUserInfo, }; }, ); const basePreRequestUserStateForSingleKeyserverSelector: ( keyserverID: string, ) => (state: AppState) => PreRequestUserState = keyserverID => createSelector( (state: AppState) => state.currentUserInfo, cookieSelector(keyserverID), sessionIDSelector(keyserverID), ( currentUserInfo: ?CurrentUserInfo, cookie: ?string, sessionID: ?string, ) => ({ currentUserInfo, cookiesAndSessions: { [keyserverID]: { cookie, sessionID } }, }), ); const preRequestUserStateForSingleKeyserverSelector: ( keyserverID: string, ) => (state: AppState) => PreRequestUserState = _memoize( basePreRequestUserStateForSingleKeyserverSelector, ); const createPreRequestUserKeyserverSessionInfoSelector: () => KeyserverInfo => PreRequestUserKeyserverSessionInfo = () => createSelector( (keyserverInfo: KeyserverInfo) => keyserverInfo.cookie, (keyserverInfo: KeyserverInfo) => keyserverInfo.sessionID, (cookie: ?string, sessionID: ?string) => ({ cookie, sessionID }), ); function usePreRequestUserState(): PreRequestUserState { const currentUserInfo = useSelector(state => state.currentUserInfo); const keyserverInfos = useSelector( state => state.keyserverStore.keyserverInfos, ); const cookiesAndSessions = useDerivedObject( keyserverInfos, createPreRequestUserKeyserverSessionInfoSelector, ); return React.useMemo( () => ({ currentUserInfo, cookiesAndSessions, }), [currentUserInfo, cookiesAndSessions], ); } export { - logInExtraInfoSelector, + legacyLogInExtraInfoSelector, preRequestUserStateForSingleKeyserverSelector, usePreRequestUserState, }; diff --git a/lib/types/account-types.js b/lib/types/account-types.js index 01957a5af..9b90ea936 100644 --- a/lib/types/account-types.js +++ b/lib/types/account-types.js @@ -1,301 +1,301 @@ // @flow import t, { type TInterface } from 'tcomb'; import type { SignedIdentityKeysBlob } from './crypto-types.js'; import type { PlatformDetails } from './device-types.js'; import type { CalendarQuery, CalendarResult, RawEntryInfo, } from './entry-types.js'; import { type RawMessageInfo, type MessageTruncationStatuses, type GenericMessagesResult, } from './message-types.js'; import type { PreRequestUserState, IdentityCallPreRequestUserState, } from './session-types.js'; import { type MixedRawThreadInfos, type RawThreadInfos, } from './thread-types.js'; import type { CurrentUserInfo, UserInfo, LoggedOutUserInfo, LoggedInUserInfo, } from './user-types'; import type { PolicyType } from '../facts/policies.js'; import { values } from '../utils/objects.js'; import { tShape } from '../utils/validation-utils.js'; export type ResetPasswordRequest = { +usernameOrEmail: string, }; export type LogOutResult = { +currentUserInfo: ?LoggedOutUserInfo, +preRequestUserState: IdentityCallPreRequestUserState, }; export type KeyserverLogOutResult = $ReadOnly<{ +currentUserInfo: ?LoggedOutUserInfo, +preRequestUserState: PreRequestUserState, +keyserverIDs: $ReadOnlyArray, }>; export type LogOutResponse = { +currentUserInfo: LoggedOutUserInfo, }; export type LegacyRegisterInfo = { - ...LogInExtraInfo, + ...LegacyLogInExtraInfo, +username: string, +password: string, }; export type DeviceTokenUpdateRequest = { +deviceToken: string, }; type DeviceTokenUpdateInput = { +[keyserverID: string]: DeviceTokenUpdateRequest, }; export type RegisterRequest = { +username: string, +email?: empty, +password: string, +calendarQuery?: ?CalendarQuery, +deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, +platformDetails: PlatformDetails, +primaryIdentityPublicKey?: empty, +signedIdentityKeysBlob?: SignedIdentityKeysBlob, +initialNotificationsEncryptedMessage?: string, }; export type RegisterResponse = { +id: string, +rawMessageInfos: $ReadOnlyArray, +currentUserInfo: LoggedInUserInfo, +cookieChange: { +threadInfos: MixedRawThreadInfos, +userInfos: $ReadOnlyArray, }, }; export type LegacyRegisterResult = { +currentUserInfo: LoggedInUserInfo, +rawMessageInfos: $ReadOnlyArray, +threadInfos: RawThreadInfos, +userInfos: $ReadOnlyArray, +calendarQuery: CalendarQuery, }; export const recoveryFromReduxActionSources = 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', refetchUserDataAfterAcknowledgment: 'REFETCH_USER_DATA_AFTER_ACKNOWLEDGMENT', socketNotLoggedIn: 'SOCKET_NOT_LOGGED_IN', }); export type RecoveryFromReduxActionSource = $Values< typeof recoveryFromReduxActionSources, >; export const recoveryFromDataHandlerActionSources = Object.freeze({ //sqliteOpFailure: 'SQLITE_OP_FAILURE', (DEPRECATED) sqliteLoadFailure: 'SQLITE_LOAD_FAILURE', corruptedDatabaseDeletion: 'CORRUPTED_DATABASE_DELETION', }); export type RecoveryFromDataHandlerActionSource = $Values< typeof recoveryFromDataHandlerActionSources, >; export type RecoveryActionSource = | RecoveryFromReduxActionSource | RecoveryFromDataHandlerActionSource; export const logInActionSources = Object.freeze({ logInFromWebForm: 'LOG_IN_FROM_WEB_FORM', logInFromNativeForm: 'LOG_IN_FROM_NATIVE_FORM', logInFromNativeSIWE: 'LOG_IN_FROM_NATIVE_SIWE', keyserverAuthFromNative: 'KEYSERVER_AUTH_FROM_NATIVE', keyserverAuthFromWeb: 'KEYSERVER_AUTH_FROM_WEB', }); export type LogInActionSource = $Values; export const authActionSources = Object.freeze({ ...recoveryFromReduxActionSources, ...recoveryFromDataHandlerActionSources, ...logInActionSources, }); export type AuthActionSource = LogInActionSource | RecoveryActionSource; export type LegacyLogInStartingPayload = { +calendarQuery: CalendarQuery, +authActionSource?: AuthActionSource, }; -export type LogInExtraInfo = { +export type LegacyLogInExtraInfo = { +calendarQuery: CalendarQuery, +deviceTokenUpdateRequest: DeviceTokenUpdateInput, +signedIdentityKeysBlob?: SignedIdentityKeysBlob, +initialNotificationsEncryptedMessage?: string, +preRequestUserInfo: ?CurrentUserInfo, }; export type LegacyLogInInfo = { - ...LogInExtraInfo, + ...LegacyLogInExtraInfo, +username: string, +password: string, +authActionSource: AuthActionSource, +keyserverIDs?: $ReadOnlyArray, }; export type LogInRequest = { +usernameOrEmail?: ?string, +username?: ?string, +password: string, +calendarQuery?: ?CalendarQuery, +deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, +platformDetails: PlatformDetails, +watchedIDs: $ReadOnlyArray, +source?: AuthActionSource, +primaryIdentityPublicKey?: empty, +signedIdentityKeysBlob?: SignedIdentityKeysBlob, +initialNotificationsEncryptedMessage?: string, }; export type ServerLogInResponse = { +currentUserInfo: LoggedInUserInfo, +rawMessageInfos: $ReadOnlyArray, +truncationStatuses: MessageTruncationStatuses, +userInfos: $ReadOnlyArray, +rawEntryInfos?: ?$ReadOnlyArray, +serverTime: number, +cookieChange: { +threadInfos: MixedRawThreadInfos, +userInfos: $ReadOnlyArray, }, +notAcknowledgedPolicies?: $ReadOnlyArray, }; export type ClientLogInResponse = $ReadOnly<{ ...ServerLogInResponse, +cookieChange: $ReadOnly<{ ...$PropertyType, threadInfos: RawThreadInfos, }>, }>; export type LegacyLogInResult = { +threadInfos: RawThreadInfos, +currentUserInfo: LoggedInUserInfo, +messagesResult: GenericMessagesResult, +userInfos: $ReadOnlyArray, +calendarResult: CalendarResult, +updatesCurrentAsOf: { +[keyserverID: string]: number }, +authActionSource: AuthActionSource, +notAcknowledgedPolicies?: $ReadOnlyArray, +preRequestUserInfo: ?CurrentUserInfo, }; export type KeyserverAuthResult = { +threadInfos: RawThreadInfos, +currentUserInfo?: ?LoggedInUserInfo, +messagesResult: GenericMessagesResult, +userInfos: $ReadOnlyArray, +calendarResult: CalendarResult, +updatesCurrentAsOf: { +[keyserverID: string]: number }, +authActionSource: AuthActionSource, +notAcknowledgedPolicies?: ?$ReadOnlyArray, +preRequestUserInfo: ?CurrentUserInfo, }; type KeyserverRequestData = { +initialContentEncryptedMessage: string, +initialNotificationsEncryptedMessage: string, }; export type KeyserverAuthInfo = { +userID: string, +deviceID: string, +doNotRegister: boolean, +calendarQuery: CalendarQuery, +deviceTokenUpdateInput: DeviceTokenUpdateInput, +authActionSource: AuthActionSource, +keyserverData: { +[keyserverID: string]: KeyserverRequestData }, }; export type KeyserverAuthRequest = $ReadOnly<{ ...KeyserverRequestData, +userID: string, +deviceID: string, +doNotRegister: boolean, +calendarQuery: CalendarQuery, +deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, +watchedIDs: $ReadOnlyArray, +platformDetails: PlatformDetails, +source?: AuthActionSource, }>; 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 const notificationTypes = Object.freeze({ FOCUSED: 'focused', BADGE_ONLY: 'badge_only', BACKGROUND: 'background', }); export type NotificationTypes = $Values; export const notificationTypeValues: $ReadOnlyArray = values(notificationTypes); export type DefaultNotificationPayload = { +default_user_notifications: ?NotificationTypes, }; export const defaultNotificationPayloadValidator: TInterface = tShape({ default_user_notifications: t.maybe(t.enums.of(notificationTypeValues)), }); export type ClaimUsernameResponse = { +message: string, +signature: string, }; diff --git a/lib/types/siwe-types.js b/lib/types/siwe-types.js index 0da3fe9c6..42e9d1a6e 100644 --- a/lib/types/siwe-types.js +++ b/lib/types/siwe-types.js @@ -1,150 +1,150 @@ // @flow -import type { LogInExtraInfo } from './account-types.js'; +import type { LegacyLogInExtraInfo } from './account-types.js'; import type { SignedIdentityKeysBlob } from './crypto-types.js'; import { type DeviceTokenUpdateRequest, type PlatformDetails, } from './device-types.js'; import { type CalendarQuery } from './entry-types.js'; export type SIWENonceResponse = { +nonce: string, }; export type SIWEAuthRequest = { +message: string, +signature: string, +calendarQuery: CalendarQuery, +deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest, +platformDetails: PlatformDetails, +watchedIDs: $ReadOnlyArray, +signedIdentityKeysBlob?: ?SignedIdentityKeysBlob, +initialNotificationsEncryptedMessage?: string, +doNotRegister?: boolean, }; -export type SIWEAuthServerCall = { +export type LegacySIWEAuthServerCall = { +message: string, +signature: string, +doNotRegister?: boolean, - ...LogInExtraInfo, + ...LegacyLogInExtraInfo, }; export type SIWESocialProof = { +siweMessage: string, +siweMessageSignature: string, }; // This is a message that the rendered webpage (landing/siwe.react.js) uses to // communicate back to the React Native WebView that is rendering it // (native/account/siwe-panel.react.js) export type SIWEWebViewMessage = | { +type: 'siwe_success', +address: string, +message: string, +signature: string, } | { +type: 'siwe_closed', } | { +type: 'walletconnect_modal_update', +state: 'open', +height: number, } | { +type: 'walletconnect_modal_update', +state: 'closed', }; export type SIWEMessage = { // RFC 4501 dns authority that is requesting the signing. +domain: string, // Ethereum address performing the signing conformant to capitalization // encoded checksum specified in EIP-55 where applicable. +address: string, // Human-readable ASCII assertion that the user will sign, and it must not // contain `\n`. +statement?: string, // RFC 3986 URI referring to the resource that is the subject of the signing // (as in the __subject__ of a claim). +uri: string, // Current version of the message. +version: string, // EIP-155 Chain ID to which the session is bound, and the network where // Contract Accounts must be resolved. +chainId: number, // Randomized token used to prevent replay attacks, at least 8 alphanumeric // characters. +nonce: string, // ISO 8601 datetime string of the current time. +issuedAt: string, // ISO 8601 datetime string that, if present, indicates when the signed // authentication message is no longer valid. +expirationTime?: string, // ISO 8601 datetime string that, if present, indicates when the signed // authentication message will become valid. +notBefore?: string, // System-specific identifier that may be used to uniquely refer to the // sign-in request. +requestId?: string, // List of information or references to information the user wishes to have // resolved as part of authentication by the relying party. They are // expressed as RFC 3986 URIs separated by `\n- `. +resources?: $ReadOnlyArray, // @deprecated // Signature of the message signed by the wallet. // // This field will be removed in future releases, an additional parameter // was added to the validate function were the signature goes to validate // the message. +signature?: string, // @deprecated // Type of sign message to be generated. // // This field will be removed in future releases and will rely on the // message version. +type?: 'Personal signature', +verify: ({ +signature: string, ... }) => Promise, +toMessage: () => string, }; export type SIWEResult = { +address: string, +message: string, +signature: string, }; export type IdentityWalletRegisterInput = { +address: string, +message: string, +signature: string, +fid?: ?string, }; export const SIWEMessageTypes = Object.freeze({ MSG_AUTH: 'msg_auth', MSG_BACKUP: 'msg_backup', }); export type SIWEMessageType = $Values; export type SIWEBackupSecrets = { +message: string, +signature: string, }; diff --git a/native/account/legacy-recover-keyserver-session.js b/native/account/legacy-recover-keyserver-session.js index 0dc9a20e5..8d85bd1f7 100644 --- a/native/account/legacy-recover-keyserver-session.js +++ b/native/account/legacy-recover-keyserver-session.js @@ -1,66 +1,66 @@ // @flow import { legacyLogInActionTypes, legacyLogInRawAction, } from 'lib/actions/user-actions.js'; import type { CallSingleKeyserverEndpoint } from 'lib/keyserver-conn/call-single-keyserver-endpoint.js'; import { type CallKeyserverEndpoint, CANCELLED_ERROR, } from 'lib/keyserver-conn/keyserver-conn-types.js'; import type { InitialNotifMessageOptions } from 'lib/shared/crypto-utils.js'; import type { RecoveryActionSource } from 'lib/types/account-types.js'; import type { DispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { fetchNativeKeychainCredentials } from './native-credentials.js'; import { store } from '../redux/redux-setup.js'; -import { nativeLogInExtraInfoSelector } from '../selectors/account-selectors.js'; +import { nativeLegacyLogInExtraInfoSelector } from '../selectors/account-selectors.js'; async function resolveKeyserverSessionInvalidationUsingNativeCredentials( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, callKeyserverEndpoint: CallKeyserverEndpoint, dispatchActionPromise: DispatchActionPromise, recoveryActionSource: RecoveryActionSource, keyserverID: string, getInitialNotificationsEncryptedMessage: ( ?InitialNotifMessageOptions, ) => Promise, hasBeenCancelled: () => boolean, ) { const keychainCredentials = await fetchNativeKeychainCredentials(); if (!keychainCredentials || hasBeenCancelled()) { return; } const [baseExtraInfo, initialNotificationsEncryptedMessage] = await Promise.all([ - nativeLogInExtraInfoSelector(store.getState())(), + nativeLegacyLogInExtraInfoSelector(store.getState())(), getInitialNotificationsEncryptedMessage({ callSingleKeyserverEndpoint, }), ]); if (hasBeenCancelled()) { throw new Error(CANCELLED_ERROR); } const extraInfo = { ...baseExtraInfo, initialNotificationsEncryptedMessage }; const { calendarQuery } = extraInfo; const startingPayload = { calendarQuery, authActionSource: recoveryActionSource, }; await dispatchActionPromise( legacyLogInActionTypes, legacyLogInRawAction(callKeyserverEndpoint)({ ...keychainCredentials, ...extraInfo, authActionSource: recoveryActionSource, keyserverIDs: [keyserverID], }), undefined, startingPayload, ); } export { resolveKeyserverSessionInvalidationUsingNativeCredentials }; diff --git a/native/account/legacy-register-panel.react.js b/native/account/legacy-register-panel.react.js index bdd358139..598fab641 100644 --- a/native/account/legacy-register-panel.react.js +++ b/native/account/legacy-register-panel.react.js @@ -1,519 +1,521 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { Text, View, StyleSheet, Platform, Keyboard, Linking, } from 'react-native'; import Animated from 'react-native-reanimated'; import { setDataLoadedActionType } from 'lib/actions/client-db-store-actions.js'; import { legacyKeyserverRegisterActionTypes, legacyKeyserverRegister, getOlmSessionInitializationDataActionTypes, } from 'lib/actions/user-actions.js'; import { useLegacyAshoatKeyserverCall } from 'lib/keyserver-conn/legacy-keyserver-call.js'; import { createLoadingStatusSelector, combineLoadingStatuses, } from 'lib/selectors/loading-selectors.js'; import { validUsernameRegex } from 'lib/shared/account-utils.js'; import { useInitialNotificationsEncryptedMessage } from 'lib/shared/crypto-utils.js'; import type { LegacyRegisterInfo, - LogInExtraInfo, + LegacyLogInExtraInfo, LegacyRegisterResult, LegacyLogInStartingPayload, } from 'lib/types/account-types.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import type { Dispatch } from 'lib/types/redux-types.js'; import { useDispatchActionPromise, type DispatchActionPromise, } from 'lib/utils/redux-promise-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { TextInput } from './modal-components.react.js'; import { setNativeCredentials } from './native-credentials.js'; import { PanelButton, Panel } from './panel-components.react.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import SWMansionIcon from '../components/swmansion-icon.react.js'; import { useSelector } from '../redux/redux-utils.js'; -import { nativeLogInExtraInfoSelector } from '../selectors/account-selectors.js'; +import { nativeLegacyLogInExtraInfoSelector } from '../selectors/account-selectors.js'; import type { KeyPressEvent } from '../types/react-native.js'; import { AppOutOfDateAlertDetails, UsernameReservedAlertDetails, UsernameTakenAlertDetails, UnknownErrorAlertDetails, } from '../utils/alert-messages.js'; import Alert from '../utils/alert.js'; import { type StateContainer } from '../utils/state-container.js'; type WritableLegacyRegisterState = { usernameInputText: string, passwordInputText: string, confirmPasswordInputText: string, }; export type LegacyRegisterState = $ReadOnly; type BaseProps = { +setActiveAlert: (activeAlert: boolean) => void, +opacityValue: Animated.Node, +legacyRegisterState: StateContainer, }; type Props = { ...BaseProps, +loadingStatus: LoadingStatus, - +logInExtraInfo: () => Promise, + +legacyLogInExtraInfo: () => Promise, +dispatch: Dispatch, +dispatchActionPromise: DispatchActionPromise, +legacyRegister: ( registerInfo: LegacyRegisterInfo, ) => Promise, +getInitialNotificationsEncryptedMessage: () => Promise, }; type State = { +confirmPasswordFocused: boolean, }; class LegacyRegisterPanel extends React.PureComponent { state: State = { confirmPasswordFocused: false, }; usernameInput: ?TextInput; passwordInput: ?TextInput; confirmPasswordInput: ?TextInput; passwordBeingAutoFilled = false; render(): React.Node { let confirmPasswordTextInputExtraProps; if ( Platform.OS !== 'ios' || this.state.confirmPasswordFocused || this.props.legacyRegisterState.state.confirmPasswordInputText.length > 0 ) { confirmPasswordTextInputExtraProps = { secureTextEntry: true, textContentType: 'password', }; } let onPasswordKeyPress; if (Platform.OS === 'ios') { onPasswordKeyPress = this.onPasswordKeyPress; } const privatePolicyNotice = ( By signing up, you agree to our{' '} Terms {' & '} Privacy Policy . ); 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 = () => { void Linking.openURL('https://comm.app/terms'); }; onPrivacyPolicyPressed = () => { void Linking.openURL('https://comm.app/privacy'); }; onChangeUsernameInputText = (text: string) => { this.props.legacyRegisterState.setState({ usernameInputText: text }); }; onChangePasswordInputText = (text: string) => { const stateUpdate: Partial = {}; stateUpdate.passwordInputText = text; if (this.passwordBeingAutoFilled) { this.passwordBeingAutoFilled = false; stateUpdate.confirmPasswordInputText = text; } this.props.legacyRegisterState.setState(stateUpdate); }; onPasswordKeyPress = (event: KeyPressEvent) => { const { key } = event.nativeEvent; if ( key.length > 1 && key !== 'Backspace' && key !== 'Enter' && this.props.legacyRegisterState.state.confirmPasswordInputText.length === 0 ) { this.passwordBeingAutoFilled = true; } }; onChangeConfirmPasswordInputText = (text: string) => { this.props.legacyRegisterState.setState({ confirmPasswordInputText: text }); }; onConfirmPasswordFocus = () => { this.setState({ confirmPasswordFocused: true }); }; onSubmit = async () => { this.props.setActiveAlert(true); if (this.props.legacyRegisterState.state.passwordInputText === '') { Alert.alert( 'Empty password', 'Password cannot be empty', [{ text: 'OK', onPress: this.onPasswordAlertAcknowledged }], { cancelable: false }, ); } else if ( this.props.legacyRegisterState.state.passwordInputText !== this.props.legacyRegisterState.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.legacyRegisterState.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 = await this.props.logInExtraInfo(); + const extraInfo = await this.props.legacyLogInExtraInfo(); const initialNotificationsEncryptedMessage = await this.props.getInitialNotificationsEncryptedMessage(); void this.props.dispatchActionPromise( legacyKeyserverRegisterActionTypes, this.legacyRegisterAction({ ...extraInfo, initialNotificationsEncryptedMessage, }), undefined, ({ calendarQuery: extraInfo.calendarQuery, }: LegacyLogInStartingPayload), ); } }; onPasswordAlertAcknowledged = () => { this.props.setActiveAlert(false); this.props.legacyRegisterState.setState( { passwordInputText: '', confirmPasswordInputText: '', }, () => { invariant(this.passwordInput, 'ref should exist'); this.passwordInput.focus(); }, ); }; onUsernameAlertAcknowledged = () => { this.props.setActiveAlert(false); this.props.legacyRegisterState.setState( { usernameInputText: '', }, () => { invariant(this.usernameInput, 'ref should exist'); this.usernameInput.focus(); }, ); }; async legacyRegisterAction( - extraInfo: LogInExtraInfo, + extraInfo: LegacyLogInExtraInfo, ): Promise { try { const result = await this.props.legacyRegister({ ...extraInfo, username: this.props.legacyRegisterState.state.usernameInputText, password: this.props.legacyRegisterState.state.passwordInputText, }); this.props.setActiveAlert(false); this.props.dispatch({ type: setDataLoadedActionType, payload: { dataLoaded: true, }, }); await setNativeCredentials({ username: result.currentUserInfo.username, password: this.props.legacyRegisterState.state.passwordInputText, }); return result; } catch (e) { if (e.message === 'username_reserved') { Alert.alert( UsernameReservedAlertDetails.title, UsernameReservedAlertDetails.message, [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }], { cancelable: false }, ); } else if (e.message === 'username_taken') { Alert.alert( UsernameTakenAlertDetails.title, UsernameTakenAlertDetails.message, [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }], { cancelable: false }, ); } else if (e.message === 'client_version_unsupported') { Alert.alert( AppOutOfDateAlertDetails.title, AppOutOfDateAlertDetails.message, [{ text: 'OK', onPress: this.onAppOutOfDateAlertAcknowledged }], { cancelable: false }, ); } else { Alert.alert( UnknownErrorAlertDetails.title, UnknownErrorAlertDetails.message, [{ text: 'OK', onPress: this.onUnknownErrorAlertAcknowledged }], { cancelable: false }, ); } throw e; } } onUnknownErrorAlertAcknowledged = () => { this.props.setActiveAlert(false); this.props.legacyRegisterState.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 registerLoadingStatusSelector = createLoadingStatusSelector( legacyKeyserverRegisterActionTypes, ); const olmSessionInitializationDataLoadingStatusSelector = createLoadingStatusSelector(getOlmSessionInitializationDataActionTypes); const ConnectedLegacyRegisterPanel: React.ComponentType = React.memo(function ConnectedLegacyRegisterPanel( props: BaseProps, ) { const registerLoadingStatus = useSelector(registerLoadingStatusSelector); const olmSessionInitializationDataLoadingStatus = useSelector( olmSessionInitializationDataLoadingStatusSelector, ); const loadingStatus = combineLoadingStatuses( registerLoadingStatus, olmSessionInitializationDataLoadingStatus, ); - const logInExtraInfo = useSelector(nativeLogInExtraInfoSelector); + const legacyLogInExtraInfo = useSelector( + nativeLegacyLogInExtraInfoSelector, + ); const dispatch = useDispatch(); const dispatchActionPromise = useDispatchActionPromise(); const callLegacyRegister = useLegacyAshoatKeyserverCall( legacyKeyserverRegister, ); const getInitialNotificationsEncryptedMessage = useInitialNotificationsEncryptedMessage(authoritativeKeyserverID); return ( ); }); export default ConnectedLegacyRegisterPanel; diff --git a/native/account/log-in-panel.react.js b/native/account/log-in-panel.react.js index a4a99cd81..030cc8483 100644 --- a/native/account/log-in-panel.react.js +++ b/native/account/log-in-panel.react.js @@ -1,479 +1,481 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View, StyleSheet, Keyboard, Platform } from 'react-native'; import Animated from 'react-native-reanimated'; import { legacyLogInActionTypes, useLegacyLogIn, getOlmSessionInitializationDataActionTypes, } from 'lib/actions/user-actions.js'; import { usePasswordLogIn } from 'lib/hooks/login-hooks.js'; import { createLoadingStatusSelector, combineLoadingStatuses, } from 'lib/selectors/loading-selectors.js'; import { validEmailRegex, oldValidUsernameRegex, } from 'lib/shared/account-utils.js'; import { useInitialNotificationsEncryptedMessage } from 'lib/shared/crypto-utils.js'; import { type LegacyLogInInfo, - type LogInExtraInfo, + type LegacyLogInExtraInfo, type LegacyLogInResult, type LegacyLogInStartingPayload, logInActionSources, } from 'lib/types/account-types.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import { getMessageForException } from 'lib/utils/errors.js'; import { useDispatchActionPromise, type DispatchActionPromise, } from 'lib/utils/redux-promise-utils.js'; import { usingCommServicesAccessToken } from 'lib/utils/services-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 { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import SWMansionIcon from '../components/swmansion-icon.react.js'; import { useSelector } from '../redux/redux-utils.js'; -import { nativeLogInExtraInfoSelector } from '../selectors/account-selectors.js'; +import { nativeLegacyLogInExtraInfoSelector } from '../selectors/account-selectors.js'; import type { KeyPressEvent } from '../types/react-native.js'; import { AppOutOfDateAlertDetails, UnknownErrorAlertDetails, UserNotFoundAlertDetails, } from '../utils/alert-messages.js'; import Alert from '../utils/alert.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: () => Promise, + +legacyLogInExtraInfo: () => Promise, +dispatchActionPromise: DispatchActionPromise, +legacyLogIn: (logInInfo: LegacyLogInInfo) => Promise, +identityPasswordLogIn: (username: string, password: string) => Promise, +getInitialNotificationsEncryptedMessage: () => Promise, }; type State = { +logInPending: boolean, }; class LogInPanel extends React.PureComponent { usernameInput: ?TextInput; passwordInput: ?PasswordInput; state: State = { logInPending: false }; componentDidMount() { void 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 ( ); } getLoadingStatus(): LoadingStatus { if (this.props.loadingStatus === 'loading') { return 'loading'; } if (this.state.logInPending) { return 'loading'; } return 'inactive'; } 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: () => 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(); if (usingCommServicesAccessToken) { await this.identityPasswordLogIn(); return; } - const extraInfo = await this.props.logInExtraInfo(); + const extraInfo = await this.props.legacyLogInExtraInfo(); const initialNotificationsEncryptedMessage = await this.props.getInitialNotificationsEncryptedMessage(); void this.props.dispatchActionPromise( legacyLogInActionTypes, this.legacyLogInAction({ ...extraInfo, initialNotificationsEncryptedMessage, }), undefined, ({ calendarQuery: extraInfo.calendarQuery }: LegacyLogInStartingPayload), ); }; async legacyLogInAction( - extraInfo: LogInExtraInfo, + extraInfo: LegacyLogInExtraInfo, ): Promise { try { const result = await this.props.legacyLogIn({ ...extraInfo, username: this.usernameInputText, password: this.passwordInputText, authActionSource: logInActionSources.logInFromNativeForm, }); this.props.setActiveAlert(false); await setNativeCredentials({ username: result.currentUserInfo.username, password: this.passwordInputText, }); return result; } catch (e) { if (e.message === 'invalid_credentials') { Alert.alert( UserNotFoundAlertDetails.title, UserNotFoundAlertDetails.message, [{ text: 'OK', onPress: this.onUnsuccessfulLoginAlertAckowledged }], { cancelable: false }, ); } else if (e.message === 'client_version_unsupported') { Alert.alert( AppOutOfDateAlertDetails.title, AppOutOfDateAlertDetails.message, [{ text: 'OK', onPress: this.onAppOutOfDateAlertAcknowledged }], { cancelable: false }, ); } else { Alert.alert( UnknownErrorAlertDetails.title, UnknownErrorAlertDetails.message, [{ text: 'OK', onPress: this.onUnknownErrorAlertAcknowledged }], { cancelable: false }, ); } throw e; } } async identityPasswordLogIn(): Promise { if (this.state.logInPending) { return; } this.setState({ logInPending: true }); try { await this.props.identityPasswordLogIn( this.usernameInputText, this.passwordInputText, ); this.props.setActiveAlert(false); await setNativeCredentials({ username: this.usernameInputText, password: this.passwordInputText, }); } catch (e) { const messageForException = getMessageForException(e); if ( messageForException === 'user not found' || messageForException === 'login failed' ) { Alert.alert( UserNotFoundAlertDetails.title, UserNotFoundAlertDetails.message, [{ text: 'OK', onPress: this.onUnsuccessfulLoginAlertAckowledged }], { cancelable: false }, ); } else if (messageForException === 'Unsupported version') { Alert.alert( AppOutOfDateAlertDetails.title, AppOutOfDateAlertDetails.message, [{ text: 'OK', onPress: this.onAppOutOfDateAlertAcknowledged }], { cancelable: false }, ); } else { Alert.alert( UnknownErrorAlertDetails.title, UnknownErrorAlertDetails.message, [{ text: 'OK', onPress: this.onUnknownErrorAlertAcknowledged }], { cancelable: false }, ); } throw e; } finally { this.setState({ logInPending: false }); } } 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 logInLoadingStatusSelector = createLoadingStatusSelector( legacyLogInActionTypes, ); const olmSessionInitializationDataLoadingStatusSelector = createLoadingStatusSelector(getOlmSessionInitializationDataActionTypes); const ConnectedLogInPanel: React.ComponentType = React.memo(function ConnectedLogInPanel(props: BaseProps) { const logInLoadingStatus = useSelector(logInLoadingStatusSelector); const olmSessionInitializationDataLoadingStatus = useSelector( olmSessionInitializationDataLoadingStatusSelector, ); const loadingStatus = combineLoadingStatuses( logInLoadingStatus, olmSessionInitializationDataLoadingStatus, ); - const logInExtraInfo = useSelector(nativeLogInExtraInfoSelector); + const legacyLogInExtraInfo = useSelector( + nativeLegacyLogInExtraInfoSelector, + ); const dispatchActionPromise = useDispatchActionPromise(); const callLegacyLogIn = useLegacyLogIn(); const callIdentityPasswordLogIn = usePasswordLogIn(); const getInitialNotificationsEncryptedMessage = useInitialNotificationsEncryptedMessage(authoritativeKeyserverID); return ( ); }); export default ConnectedLogInPanel; diff --git a/native/account/registration/registration-server-call.js b/native/account/registration/registration-server-call.js index a7ec508f6..a4871b0d2 100644 --- a/native/account/registration/registration-server-call.js +++ b/native/account/registration/registration-server-call.js @@ -1,425 +1,425 @@ // @flow import * as React from 'react'; import { setDataLoadedActionType } from 'lib/actions/client-db-store-actions.js'; import { setSyncedMetadataEntryActionType } from 'lib/actions/synced-metadata-actions.js'; import { legacyKeyserverRegisterActionTypes, legacyKeyserverRegister, useIdentityPasswordRegister, identityRegisterActionTypes, } from 'lib/actions/user-actions.js'; import { useKeyserverAuth } from 'lib/keyserver-conn/keyserver-auth.js'; import { useLegacyAshoatKeyserverCall } from 'lib/keyserver-conn/legacy-keyserver-call.js'; import { isLoggedInToKeyserver } from 'lib/selectors/user-selectors.js'; import { type LegacyLogInStartingPayload, logInActionSources, } from 'lib/types/account-types.js'; import { syncedMetadataNames } from 'lib/types/synced-metadata-types.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js'; import { setURLPrefix } from 'lib/utils/url-utils.js'; import type { RegistrationServerCallInput, UsernameAccountSelection, AvatarData, } from './registration-types.js'; import { authoritativeKeyserverID } from '../../authoritative-keyserver.js'; import { useNativeSetUserAvatar, useUploadSelectedMedia, } from '../../avatars/avatar-hooks.js'; import { commCoreModule } from '../../native-modules.js'; import { useSelector } from '../../redux/redux-utils.js'; -import { nativeLogInExtraInfoSelector } from '../../selectors/account-selectors.js'; +import { nativeLegacyLogInExtraInfoSelector } from '../../selectors/account-selectors.js'; import { AppOutOfDateAlertDetails, UsernameReservedAlertDetails, UsernameTakenAlertDetails, UnknownErrorAlertDetails, } from '../../utils/alert-messages.js'; import Alert from '../../utils/alert.js'; import { setNativeCredentials } from '../native-credentials.js'; import { useLegacySIWEServerCall, useIdentityWalletRegisterCall, } from '../siwe-hooks.js'; // We can't just do everything in one async callback, since the server calls // would get bound to Redux state from before the registration. The registration // flow has multiple steps where critical Redux state is changed, where // subsequent steps depend on accessing the updated Redux state. // To address this, we break the registration process up into multiple steps. // When each step completes we update the currentStep state, and we have Redux // selectors that trigger useEffects for subsequent steps when relevant data // starts to appear in Redux. type CurrentStep = | { +step: 'inactive' } | { +step: 'identity_registration_dispatched', +clearCachedSelections: () => void, +avatarData: ?AvatarData, +credentialsToSave: ?{ +username: string, +password: string }, +resolve: () => void, +reject: Error => void, } | { +step: 'authoritative_keyserver_registration_dispatched', +clearCachedSelections: () => void, +avatarData: ?AvatarData, +credentialsToSave: ?{ +username: string, +password: string }, +resolve: () => void, +reject: Error => void, }; const inactiveStep = { step: 'inactive' }; function useRegistrationServerCall(): RegistrationServerCallInput => Promise { const [currentStep, setCurrentStep] = React.useState(inactiveStep); // STEP 1: ACCOUNT REGISTRATION - const logInExtraInfo = useSelector(nativeLogInExtraInfoSelector); + const legacyLogInExtraInfo = useSelector(nativeLegacyLogInExtraInfoSelector); const dispatchActionPromise = useDispatchActionPromise(); const callLegacyKeyserverRegister = useLegacyAshoatKeyserverCall( legacyKeyserverRegister, ); const callIdentityPasswordRegister = useIdentityPasswordRegister(); const identityRegisterUsernameAccount = React.useCallback( async ( accountSelection: UsernameAccountSelection, farcasterID: ?string, ) => { const identityRegisterPromise = (async () => { try { return await callIdentityPasswordRegister( accountSelection.username, accountSelection.password, farcasterID, ); } catch (e) { if (e.message === 'username reserved') { Alert.alert( UsernameReservedAlertDetails.title, UsernameReservedAlertDetails.message, ); } else if (e.message === 'username already exists') { Alert.alert( UsernameTakenAlertDetails.title, UsernameTakenAlertDetails.message, ); } else if (e.message === 'Unsupported version') { Alert.alert( AppOutOfDateAlertDetails.title, AppOutOfDateAlertDetails.message, ); } else { Alert.alert( UnknownErrorAlertDetails.title, UnknownErrorAlertDetails.message, ); } throw e; } })(); void dispatchActionPromise( identityRegisterActionTypes, identityRegisterPromise, ); await identityRegisterPromise; }, [callIdentityPasswordRegister, dispatchActionPromise], ); const legacyKeyserverRegisterUsernameAccount = React.useCallback( async ( accountSelection: UsernameAccountSelection, keyserverURL: string, ) => { - const extraInfo = await logInExtraInfo(); + const extraInfo = await legacyLogInExtraInfo(); const legacyKeyserverRegisterPromise = (async () => { try { return await callLegacyKeyserverRegister( { ...extraInfo, username: accountSelection.username, password: accountSelection.password, }, { urlPrefixOverride: keyserverURL, }, ); } catch (e) { if (e.message === 'username_reserved') { Alert.alert( UsernameReservedAlertDetails.title, UsernameReservedAlertDetails.message, ); } else if (e.message === 'username_taken') { Alert.alert( UsernameTakenAlertDetails.title, UsernameTakenAlertDetails.message, ); } else if (e.message === 'client_version_unsupported') { Alert.alert( AppOutOfDateAlertDetails.title, AppOutOfDateAlertDetails.message, ); } else { Alert.alert( UnknownErrorAlertDetails.title, UnknownErrorAlertDetails.message, ); } throw e; } })(); void dispatchActionPromise( legacyKeyserverRegisterActionTypes, legacyKeyserverRegisterPromise, undefined, ({ calendarQuery: extraInfo.calendarQuery, }: LegacyLogInStartingPayload), ); await legacyKeyserverRegisterPromise; }, - [logInExtraInfo, callLegacyKeyserverRegister, dispatchActionPromise], + [legacyLogInExtraInfo, callLegacyKeyserverRegister, dispatchActionPromise], ); const legacySiweServerCall = useLegacySIWEServerCall(); const identityWalletRegisterCall = useIdentityWalletRegisterCall(); const dispatch = useDispatch(); const returnedFunc = React.useCallback( (input: RegistrationServerCallInput) => new Promise( // eslint-disable-next-line no-async-promise-executor async (resolve, reject) => { try { if (currentStep.step !== 'inactive') { return; } const { accountSelection, avatarData, keyserverURL, farcasterID, siweBackupSecrets, clearCachedSelections, } = input; if ( accountSelection.accountType === 'username' && !usingCommServicesAccessToken ) { await legacyKeyserverRegisterUsernameAccount( accountSelection, keyserverURL, ); } else if (accountSelection.accountType === 'username') { await identityRegisterUsernameAccount( accountSelection, farcasterID, ); } else if (!usingCommServicesAccessToken) { try { await legacySiweServerCall(accountSelection, { urlPrefixOverride: keyserverURL, }); } catch (e) { Alert.alert( UnknownErrorAlertDetails.title, UnknownErrorAlertDetails.message, ); throw e; } } else { try { await identityWalletRegisterCall({ address: accountSelection.address, message: accountSelection.message, signature: accountSelection.signature, fid: farcasterID, }); } catch (e) { Alert.alert( UnknownErrorAlertDetails.title, UnknownErrorAlertDetails.message, ); throw e; } } dispatch({ type: setURLPrefix, payload: keyserverURL, }); if (farcasterID) { dispatch({ type: setSyncedMetadataEntryActionType, payload: { name: syncedMetadataNames.CURRENT_USER_FID, data: farcasterID, }, }); } if (siweBackupSecrets) { await commCoreModule.setSIWEBackupSecrets(siweBackupSecrets); } const credentialsToSave = accountSelection.accountType === 'username' ? { username: accountSelection.username, password: accountSelection.password, } : null; setCurrentStep({ step: 'identity_registration_dispatched', avatarData, clearCachedSelections, credentialsToSave, resolve, reject, }); } catch (e) { reject(e); } }, ), [ currentStep, legacyKeyserverRegisterUsernameAccount, identityRegisterUsernameAccount, legacySiweServerCall, dispatch, identityWalletRegisterCall, ], ); // STEP 2: REGISTERING ON AUTHORITATIVE KEYSERVER const keyserverAuth = useKeyserverAuth(authoritativeKeyserverID); const isRegisteredOnIdentity = useSelector( state => !!state.commServicesAccessToken && !!state.currentUserInfo && !state.currentUserInfo.anonymous, ); const registeringOnAuthoritativeKeyserverRef = React.useRef(false); React.useEffect(() => { if ( !isRegisteredOnIdentity || currentStep.step !== 'identity_registration_dispatched' || registeringOnAuthoritativeKeyserverRef.current ) { return; } registeringOnAuthoritativeKeyserverRef.current = true; const { avatarData, clearCachedSelections, credentialsToSave, resolve, reject, } = currentStep; void (async () => { try { await keyserverAuth({ authActionSource: process.env.BROWSER ? logInActionSources.keyserverAuthFromWeb : logInActionSources.keyserverAuthFromNative, setInProgress: () => {}, hasBeenCancelled: () => false, doNotRegister: false, }); setCurrentStep({ step: 'authoritative_keyserver_registration_dispatched', avatarData, clearCachedSelections, credentialsToSave, resolve, reject, }); } catch (e) { reject(e); setCurrentStep(inactiveStep); } finally { registeringOnAuthoritativeKeyserverRef.current = false; } })(); }, [currentStep, isRegisteredOnIdentity, keyserverAuth]); // STEP 3: SETTING AVATAR const uploadSelectedMedia = useUploadSelectedMedia(); const nativeSetUserAvatar = useNativeSetUserAvatar(); const isLoggedInToAuthoritativeKeyserver = useSelector( isLoggedInToKeyserver(authoritativeKeyserverID), ); const avatarBeingSetRef = React.useRef(false); React.useEffect(() => { if ( !isLoggedInToAuthoritativeKeyserver || currentStep.step !== 'authoritative_keyserver_registration_dispatched' || avatarBeingSetRef.current ) { return; } avatarBeingSetRef.current = true; const { avatarData, resolve, clearCachedSelections, credentialsToSave } = currentStep; void (async () => { try { if (!avatarData) { return; } let updateUserAvatarRequest; if (!avatarData.needsUpload) { ({ updateUserAvatarRequest } = avatarData); } else { const { mediaSelection } = avatarData; updateUserAvatarRequest = await uploadSelectedMedia(mediaSelection); if (!updateUserAvatarRequest) { return; } } await nativeSetUserAvatar(updateUserAvatarRequest); } finally { dispatch({ type: setDataLoadedActionType, payload: { dataLoaded: true, }, }); clearCachedSelections(); if (credentialsToSave) { void setNativeCredentials(credentialsToSave); } setCurrentStep(inactiveStep); avatarBeingSetRef.current = false; resolve(); } })(); }, [ currentStep, isLoggedInToAuthoritativeKeyserver, uploadSelectedMedia, nativeSetUserAvatar, dispatch, ]); return returnedFunc; } export { useRegistrationServerCall }; diff --git a/native/account/siwe-hooks.js b/native/account/siwe-hooks.js index 7d06cf503..a42d4746c 100644 --- a/native/account/siwe-hooks.js +++ b/native/account/siwe-hooks.js @@ -1,124 +1,127 @@ // @flow import * as React from 'react'; import { legacySiweAuth, legacySiweAuthActionTypes, } from 'lib/actions/siwe-actions.js'; import { identityRegisterActionTypes, useIdentityWalletRegister, } from 'lib/actions/user-actions.js'; import type { CallSingleKeyserverEndpointOptions } from 'lib/keyserver-conn/call-single-keyserver-endpoint.js'; import { useLegacyAshoatKeyserverCall } from 'lib/keyserver-conn/legacy-keyserver-call.js'; import { useInitialNotificationsEncryptedMessage } from 'lib/shared/crypto-utils.js'; import type { LegacyLogInStartingPayload, - LogInExtraInfo, + LegacyLogInExtraInfo, } from 'lib/types/account-types.js'; import type { IdentityWalletRegisterInput } from 'lib/types/siwe-types.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import { useSelector } from '../redux/redux-utils.js'; -import { nativeLogInExtraInfoSelector } from '../selectors/account-selectors.js'; +import { nativeLegacyLogInExtraInfoSelector } from '../selectors/account-selectors.js'; type SIWEServerCallParams = { +message: string, +signature: string, +doNotRegister?: boolean, ... }; function useLegacySIWEServerCall(): ( SIWEServerCallParams, ?CallSingleKeyserverEndpointOptions, ) => Promise { const legacySiweAuthCall = useLegacyAshoatKeyserverCall(legacySiweAuth); const callSIWE = React.useCallback( ( message: string, signature: string, - extraInfo: $ReadOnly<{ ...LogInExtraInfo, +doNotRegister?: boolean }>, + extraInfo: $ReadOnly<{ + ...LegacyLogInExtraInfo, + +doNotRegister?: boolean, + }>, callSingleKeyserverEndpointOptions: ?CallSingleKeyserverEndpointOptions, ) => legacySiweAuthCall( { message, signature, ...extraInfo, }, callSingleKeyserverEndpointOptions, ), [legacySiweAuthCall], ); - const logInExtraInfo = useSelector(nativeLogInExtraInfoSelector); + const legacyLogInExtraInfo = useSelector(nativeLegacyLogInExtraInfoSelector); const getInitialNotificationsEncryptedMessage = useInitialNotificationsEncryptedMessage(authoritativeKeyserverID); const dispatchActionPromise = useDispatchActionPromise(); return React.useCallback( async ( { message, signature, doNotRegister }, callSingleKeyserverEndpointOptions, ) => { - const extraInfo = await logInExtraInfo(); + const extraInfo = await legacyLogInExtraInfo(); const initialNotificationsEncryptedMessage = await getInitialNotificationsEncryptedMessage({ callSingleKeyserverEndpointOptions, }); const siwePromise = callSIWE( message, signature, { ...extraInfo, initialNotificationsEncryptedMessage, doNotRegister, }, callSingleKeyserverEndpointOptions, ); void dispatchActionPromise( legacySiweAuthActionTypes, siwePromise, undefined, ({ calendarQuery: extraInfo.calendarQuery, }: LegacyLogInStartingPayload), ); await siwePromise; }, [ - logInExtraInfo, + legacyLogInExtraInfo, dispatchActionPromise, callSIWE, getInitialNotificationsEncryptedMessage, ], ); } function useIdentityWalletRegisterCall(): IdentityWalletRegisterInput => Promise { const identityWalletRegister = useIdentityWalletRegister(); const dispatchActionPromise = useDispatchActionPromise(); return React.useCallback( async ({ address, message, signature, fid }) => { const siwePromise = identityWalletRegister( address, message, signature, fid, ); void dispatchActionPromise(identityRegisterActionTypes, siwePromise); await siwePromise; }, [dispatchActionPromise, identityWalletRegister], ); } export { useLegacySIWEServerCall, useIdentityWalletRegisterCall }; diff --git a/native/selectors/account-selectors.js b/native/selectors/account-selectors.js index 370784f5c..6d85091a3 100644 --- a/native/selectors/account-selectors.js +++ b/native/selectors/account-selectors.js @@ -1,66 +1,66 @@ // @flow import _memoize from 'lodash/memoize.js'; import { createSelector } from 'reselect'; -import { logInExtraInfoSelector } from 'lib/selectors/account-selectors.js'; +import { legacyLogInExtraInfoSelector } from 'lib/selectors/account-selectors.js'; import { currentAsOfSelector } from 'lib/selectors/keyserver-selectors.js'; -import type { LogInExtraInfo } from 'lib/types/account-types.js'; +import type { LegacyLogInExtraInfo } from 'lib/types/account-types.js'; import type { SignedIdentityKeysBlob } from 'lib/types/crypto-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 type { AppState } from '../redux/state-types.js'; import type { ConnectivityInfo } from '../types/connectivity.js'; -const nativeLogInExtraInfoSelector: ( +const nativeLegacyLogInExtraInfoSelector: ( state: AppState, -) => () => Promise = createSelector( - logInExtraInfoSelector, - (logInExtraInfo: LogInExtraInfo) => { +) => () => Promise = createSelector( + legacyLogInExtraInfoSelector, + (logInExtraInfo: LegacyLogInExtraInfo) => { const loginExtraFuncWithIdentityKey = async () => { await commCoreModule.initializeCryptoAccount(); const { blobPayload, signature } = await commCoreModule.getUserPublicKey(); const signedIdentityKeysBlob: SignedIdentityKeysBlob = { payload: blobPayload, signature, }; return { ...logInExtraInfo, signedIdentityKeysBlob, }; }; return loginExtraFuncWithIdentityKey; }, ); const baseNoDataAfterPolicyAcknowledgmentSelector: ( keyserverID: string, ) => (state: AppState) => boolean = keyserverID => createSelector( (state: AppState) => state.connectivity, currentAsOfSelector(keyserverID), (state: AppState) => state.userPolicies, ( connectivity: ConnectivityInfo, currentAsOf: number, userPolicies: UserPolicies, ) => connectivity.connected && currentAsOf === 0 && values(userPolicies).length > 0 && values(userPolicies).every(policy => policy.isAcknowledged), ); const noDataAfterPolicyAcknowledgmentSelector: ( keyserverID: string, ) => (state: AppState) => boolean = _memoize( baseNoDataAfterPolicyAcknowledgmentSelector, ); export { - nativeLogInExtraInfoSelector, + nativeLegacyLogInExtraInfoSelector, noDataAfterPolicyAcknowledgmentSelector, }; diff --git a/web/account/siwe-login-form.react.js b/web/account/siwe-login-form.react.js index a72404731..4c2c97e69 100644 --- a/web/account/siwe-login-form.react.js +++ b/web/account/siwe-login-form.react.js @@ -1,315 +1,319 @@ // @flow import '@rainbow-me/rainbowkit/styles.css'; import classNames from 'classnames'; import invariant from 'invariant'; import * as React from 'react'; import { useAccount, useWalletClient } from 'wagmi'; import { setDataLoadedActionType } from 'lib/actions/client-db-store-actions.js'; import { getSIWENonce, getSIWENonceActionTypes, legacySiweAuth, legacySiweAuthActionTypes, } from 'lib/actions/siwe-actions.js'; import { identityGenerateNonceActionTypes, useIdentityGenerateNonce, } from 'lib/actions/user-actions.js'; import ConnectedWalletInfo from 'lib/components/connected-wallet-info.react.js'; import SWMansionIcon from 'lib/components/swmansion-icon.react.js'; import stores from 'lib/facts/stores.js'; import { useWalletLogIn } from 'lib/hooks/login-hooks.js'; import { useLegacyAshoatKeyserverCall } from 'lib/keyserver-conn/legacy-keyserver-call.js'; -import { logInExtraInfoSelector } from 'lib/selectors/account-selectors.js'; +import { legacyLogInExtraInfoSelector } from 'lib/selectors/account-selectors.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import type { LegacyLogInStartingPayload, - LogInExtraInfo, + LegacyLogInExtraInfo, } from 'lib/types/account-types.js'; import { SIWEMessageTypes } from 'lib/types/siwe-types.js'; import { getMessageForException, ServerError } from 'lib/utils/errors.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js'; import { createSIWEMessage, getSIWEStatementForPublicKey, siweMessageSigningExplanationStatements, } from 'lib/utils/siwe-utils.js'; import HeaderSeparator from './header-separator.react.js'; import css from './siwe.css'; import Button from '../components/button.react.js'; import OrBreak from '../components/or-break.react.js'; import { olmAPI } from '../crypto/olm-api.js'; import LoadingIndicator from '../loading-indicator.react.js'; import { useSelector } from '../redux/redux-utils.js'; type SIWELogInError = 'account_does_not_exist'; type SIWELoginFormProps = { +cancelSIWEAuthFlow: () => void, }; const legacyGetSIWENonceLoadingStatusSelector = createLoadingStatusSelector( getSIWENonceActionTypes, ); const identityGenerateNonceLoadingStatusSelector = createLoadingStatusSelector( identityGenerateNonceActionTypes, ); const legacySiweAuthLoadingStatusSelector = createLoadingStatusSelector( legacySiweAuthActionTypes, ); function SIWELoginForm(props: SIWELoginFormProps): React.Node { const { address } = useAccount(); const { data: signer } = useWalletClient(); const dispatchActionPromise = useDispatchActionPromise(); const legacyGetSIWENonceCall = useLegacyAshoatKeyserverCall(getSIWENonce); const legacyGetSIWENonceCallLoadingStatus = useSelector( legacyGetSIWENonceLoadingStatusSelector, ); const identityGenerateNonce = useIdentityGenerateNonce(); const identityGenerateNonceLoadingStatus = useSelector( identityGenerateNonceLoadingStatusSelector, ); const siweAuthLoadingStatus = useSelector( legacySiweAuthLoadingStatusSelector, ); const legacySiweAuthCall = useLegacyAshoatKeyserverCall(legacySiweAuth); - const logInExtraInfo = useSelector(logInExtraInfoSelector); + const legacyLogInExtraInfo = useSelector(legacyLogInExtraInfoSelector); const walletLogIn = useWalletLogIn(); const [siweNonce, setSIWENonce] = React.useState(null); const siweNonceShouldBeFetched = !siweNonce && legacyGetSIWENonceCallLoadingStatus !== 'loading' && identityGenerateNonceLoadingStatus !== 'loading'; React.useEffect(() => { if (!siweNonceShouldBeFetched) { return; } if (usingCommServicesAccessToken) { void dispatchActionPromise( identityGenerateNonceActionTypes, (async () => { const response = await identityGenerateNonce(); setSIWENonce(response); })(), ); } else { void dispatchActionPromise( getSIWENonceActionTypes, (async () => { const response = await legacyGetSIWENonceCall(); setSIWENonce(response); })(), ); } }, [ dispatchActionPromise, identityGenerateNonce, legacyGetSIWENonceCall, siweNonceShouldBeFetched, ]); const callLegacySIWEAuthEndpoint = React.useCallback( - async (message: string, signature: string, extraInfo: LogInExtraInfo) => { + async ( + message: string, + signature: string, + extraInfo: LegacyLogInExtraInfo, + ) => { await olmAPI.initializeCryptoAccount(); const userPublicKey = await olmAPI.getUserPublicKey(); try { return await legacySiweAuthCall({ message, signature, signedIdentityKeysBlob: { payload: userPublicKey.blobPayload, signature: userPublicKey.signature, }, doNotRegister: true, ...extraInfo, }); } catch (e) { if ( e instanceof ServerError && e.message === 'account_does_not_exist' ) { setError('account_does_not_exist'); } throw e; } }, [legacySiweAuthCall], ); const attemptLegacySIWEAuth = React.useCallback( (message: string, signature: string) => { return dispatchActionPromise( legacySiweAuthActionTypes, - callLegacySIWEAuthEndpoint(message, signature, logInExtraInfo), + callLegacySIWEAuthEndpoint(message, signature, legacyLogInExtraInfo), undefined, ({ - calendarQuery: logInExtraInfo.calendarQuery, + calendarQuery: legacyLogInExtraInfo.calendarQuery, }: LegacyLogInStartingPayload), ); }, - [callLegacySIWEAuthEndpoint, dispatchActionPromise, logInExtraInfo], + [callLegacySIWEAuthEndpoint, dispatchActionPromise, legacyLogInExtraInfo], ); const attemptWalletLogIn = React.useCallback( async ( walletAddress: string, siweMessage: string, siweSignature: string, ) => { try { return await walletLogIn(walletAddress, siweMessage, siweSignature); } catch (e) { if (getMessageForException(e) === 'user not found') { setError('account_does_not_exist'); } throw e; } }, [walletLogIn], ); const dispatch = useDispatch(); const onSignInButtonClick = React.useCallback(async () => { invariant(signer, 'signer must be present during SIWE attempt'); invariant(siweNonce, 'nonce must be present during SIWE attempt'); await olmAPI.initializeCryptoAccount(); const { primaryIdentityPublicKeys: { ed25519 }, } = await olmAPI.getUserPublicKey(); const statement = getSIWEStatementForPublicKey( ed25519, SIWEMessageTypes.MSG_AUTH, ); const message = createSIWEMessage(address, statement, siweNonce); const signature = await signer.signMessage({ message }); if (usingCommServicesAccessToken) { await attemptWalletLogIn(address, message, signature); } else { await attemptLegacySIWEAuth(message, signature); dispatch({ type: setDataLoadedActionType, payload: { dataLoaded: true, }, }); } }, [ address, attemptLegacySIWEAuth, attemptWalletLogIn, signer, siweNonce, dispatch, ]); const { cancelSIWEAuthFlow } = props; const backButtonColor = React.useMemo( () => ({ backgroundColor: '#211E2D' }), [], ); const signInButtonColor = React.useMemo( () => ({ backgroundColor: '#6A20E3' }), [], ); const [error, setError] = React.useState(); const mainMiddleAreaClassName = classNames({ [css.mainMiddleArea]: true, [css.hidden]: !!error, }); const errorOverlayClassNames = classNames({ [css.errorOverlay]: true, [css.hidden]: !error, }); if (siweAuthLoadingStatus === 'loading' || !siweNonce) { return (
); } let errorText; if (error === 'account_does_not_exist') { errorText = ( <>

No Comm account found for that Ethereum wallet!

We require that users register on their mobile devices. Comm relies on a primary device capable of scanning QR codes in order to authorize secondary devices.

You can install our iOS app  here , or our Android app  here .

); } return (

Sign in with Ethereum

Wallet Connected

{siweMessageSigningExplanationStatements}

By signing up, you agree to our{' '} Terms of Use &{' '} Privacy Policy.

{errorText}
); } export default SIWELoginForm; diff --git a/web/account/traditional-login-form.react.js b/web/account/traditional-login-form.react.js index f343af1a8..961e1be76 100644 --- a/web/account/traditional-login-form.react.js +++ b/web/account/traditional-login-form.react.js @@ -1,247 +1,247 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { useLegacyLogIn, legacyLogInActionTypes, } from 'lib/actions/user-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import { usePasswordLogIn } from 'lib/hooks/login-hooks.js'; -import { logInExtraInfoSelector } from 'lib/selectors/account-selectors.js'; +import { legacyLogInExtraInfoSelector } from 'lib/selectors/account-selectors.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { oldValidUsernameRegex, validEmailRegex, } from 'lib/shared/account-utils.js'; import type { - LogInExtraInfo, + LegacyLogInExtraInfo, LegacyLogInStartingPayload, } from 'lib/types/account-types.js'; import { logInActionSources } from 'lib/types/account-types.js'; import { getMessageForException } from 'lib/utils/errors.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js'; import HeaderSeparator from './header-separator.react.js'; import css from './log-in-form.css'; import PasswordInput from './password-input.react.js'; import Button from '../components/button.react.js'; import { olmAPI } from '../crypto/olm-api.js'; import LoadingIndicator from '../loading-indicator.react.js'; import Input from '../modals/input.react.js'; import { useSelector } from '../redux/redux-utils.js'; const loadingStatusSelector = createLoadingStatusSelector( legacyLogInActionTypes, ); function TraditionalLoginForm(): React.Node { const legacyAuthInProgress = useSelector(loadingStatusSelector) === 'loading'; const [identityAuthInProgress, setIdentityAuthInProgress] = React.useState(false); const inputDisabled = legacyAuthInProgress || identityAuthInProgress; - const loginExtraInfo = useSelector(logInExtraInfoSelector); + const legacyLoginExtraInfo = useSelector(legacyLogInExtraInfoSelector); const callLegacyLogIn = useLegacyLogIn(); const dispatchActionPromise = useDispatchActionPromise(); const modalContext = useModalContext(); const usernameInputRef = React.useRef(); React.useEffect(() => { usernameInputRef.current?.focus(); }, []); const [username, setUsername] = React.useState(''); const onUsernameChange = React.useCallback( (e: SyntheticEvent) => { invariant(e.target instanceof HTMLInputElement, 'target not input'); setUsername(e.target.value); }, [], ); const onUsernameBlur = React.useCallback(() => { setUsername(untrimmedUsername => untrimmedUsername.trim()); }, []); const [password, setPassword] = React.useState(''); const onPasswordChange = React.useCallback( (e: SyntheticEvent) => { invariant(e.target instanceof HTMLInputElement, 'target not input'); setPassword(e.target.value); }, [], ); const [errorMessage, setErrorMessage] = React.useState(''); const legacyLogInAction = React.useCallback( - async (extraInfo: LogInExtraInfo) => { + async (extraInfo: LegacyLogInExtraInfo) => { await olmAPI.initializeCryptoAccount(); const userPublicKey = await olmAPI.getUserPublicKey(); try { const result = await callLegacyLogIn({ ...extraInfo, username, password, authActionSource: logInActionSources.logInFromWebForm, signedIdentityKeysBlob: { payload: userPublicKey.blobPayload, signature: userPublicKey.signature, }, }); modalContext.popModal(); return result; } catch (e) { setUsername(''); setPassword(''); if (getMessageForException(e) === 'invalid_credentials') { setErrorMessage('incorrect username or password'); } else { setErrorMessage('unknown error'); } usernameInputRef.current?.focus(); throw e; } }, [callLegacyLogIn, modalContext, password, username], ); const callIdentityPasswordLogIn = usePasswordLogIn(); const identityPasswordLogInAction = React.useCallback(async () => { if (identityAuthInProgress) { return; } setIdentityAuthInProgress(true); try { await callIdentityPasswordLogIn(username, password); modalContext.popModal(); } catch (e) { setUsername(''); setPassword(''); const messageForException = getMessageForException(e); if ( messageForException === 'user not found' || messageForException === 'login failed' ) { setErrorMessage('incorrect username or password'); } else { setErrorMessage('unknown error'); } usernameInputRef.current?.focus(); throw e; } finally { setIdentityAuthInProgress(false); } }, [ identityAuthInProgress, callIdentityPasswordLogIn, modalContext, password, username, ]); const onSubmit = React.useCallback( (event: SyntheticEvent) => { event.preventDefault(); if (username.search(validEmailRegex) > -1) { setUsername(''); setErrorMessage('usernames only, not emails'); usernameInputRef.current?.focus(); return; } else if (username.search(oldValidUsernameRegex) === -1) { setUsername(''); setErrorMessage('alphanumeric usernames only'); usernameInputRef.current?.focus(); return; } else if (password === '') { setErrorMessage('password is empty'); usernameInputRef.current?.focus(); return; } if (usingCommServicesAccessToken) { void identityPasswordLogInAction(); } else { void dispatchActionPromise( legacyLogInActionTypes, - legacyLogInAction(loginExtraInfo), + legacyLogInAction(legacyLoginExtraInfo), undefined, ({ - calendarQuery: loginExtraInfo.calendarQuery, + calendarQuery: legacyLoginExtraInfo.calendarQuery, }: LegacyLogInStartingPayload), ); } }, [ dispatchActionPromise, identityPasswordLogInAction, legacyLogInAction, - loginExtraInfo, + legacyLoginExtraInfo, username, password, ], ); const loginButtonContent = React.useMemo(() => { if (inputDisabled) { return ; } return 'Sign in'; }, [inputDisabled]); const signInButtonColor = React.useMemo( () => ({ backgroundColor: '#6A20E3' }), [], ); return (

Sign in to Comm

Username
Password
{errorMessage}
); } export default TraditionalLoginForm;