diff --git a/lib/actions/siwe-actions.js b/lib/actions/siwe-actions.js index 88461834b..d155415c3 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 LogInResult, + type LegacyLogInResult, logInActionSources, } from '../types/account-types.js'; import type { SIWEAuthServerCall } 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, options?: ?CallSingleKeyserverEndpointOptions, - ) => Promise) => + ) => 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/actions/user-actions.js b/lib/actions/user-actions.js index 5e2bbc30e..ad840d152 100644 --- a/lib/actions/user-actions.js +++ b/lib/actions/user-actions.js @@ -1,896 +1,898 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import type { CallSingleKeyserverEndpoint, CallSingleKeyserverEndpointOptions, } from '../keyserver-conn/call-single-keyserver-endpoint.js'; import { extractKeyserverIDFromID, sortThreadIDsPerKeyserver, sortCalendarQueryPerKeyserver, } from '../keyserver-conn/keyserver-call-utils.js'; import { useKeyserverCall } from '../keyserver-conn/keyserver-call.js'; import type { CallKeyserverEndpoint } from '../keyserver-conn/keyserver-conn-types.js'; import { usePreRequestUserState } from '../selectors/account-selectors.js'; import { getOneTimeKeyValuesFromBlob, getPrekeyValueFromBlob, } from '../shared/crypto-utils.js'; import { IdentityClientContext } from '../shared/identity-client-context.js'; import threadWatcher from '../shared/thread-watcher.js'; import { permissionsAndAuthRelatedRequestTimeout } from '../shared/timeouts.js'; import type { - LogInInfo, - LogInResult, + LegacyLogInInfo, + LegacyLogInResult, LegacyRegisterResult, LegacyRegisterInfo, UpdateUserSettingsRequest, PolicyAcknowledgmentRequest, ClaimUsernameResponse, LogInRequest, KeyserverAuthResult, KeyserverAuthInfo, KeyserverAuthRequest, ClientLogInResponse, KeyserverLogOutResult, LogOutResult, } from '../types/account-types.js'; import type { UpdateUserAvatarRequest, UpdateUserAvatarResponse, } from '../types/avatar-types.js'; import type { RawEntryInfo, CalendarQuery } from '../types/entry-types.js'; import type { IdentityAuthResult } from '../types/identity-service-types.js'; import type { RawMessageInfo, MessageTruncationStatuses, } from '../types/message-types.js'; import type { GetOlmSessionInitializationDataResponse } from '../types/request-types.js'; import type { UserSearchResult, ExactUserSearchResult, } from '../types/search-types.js'; import type { PreRequestUserState } from '../types/session-types.js'; import type { SubscriptionUpdateRequest, SubscriptionUpdateResult, } from '../types/subscription-types.js'; import type { RawThreadInfos } from '../types/thread-types'; import type { CurrentUserInfo, UserInfo, PasswordUpdate, LoggedOutUserInfo, } from '../types/user-types.js'; import { authoritativeKeyserverID } from '../utils/authoritative-keyserver.js'; import { getConfig } from '../utils/config.js'; import { useSelector } from '../utils/redux-utils.js'; import { usingCommServicesAccessToken } from '../utils/services-utils.js'; import sleep from '../utils/sleep.js'; const loggedOutUserInfo: LoggedOutUserInfo = { anonymous: true, }; export type KeyserverLogOutInput = { +preRequestUserState: PreRequestUserState, +keyserverIDs?: $ReadOnlyArray, }; const logOutActionTypes = Object.freeze({ started: 'LOG_OUT_STARTED', success: 'LOG_OUT_SUCCESS', failed: 'LOG_OUT_FAILED', }); const keyserverLogOut = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): ((input: KeyserverLogOutInput) => Promise) => async input => { const { preRequestUserState } = input; const keyserverIDs = input.keyserverIDs ?? allKeyserverIDs; const requests: { [string]: {} } = {}; for (const keyserverID of keyserverIDs) { requests[keyserverID] = {}; } let response = null; try { response = await Promise.race([ callKeyserverEndpoint('log_out', requests), (async () => { await sleep(500); throw new Error('keyserver log_out took more than 500ms'); })(), ]); } catch {} const currentUserInfo = response ? loggedOutUserInfo : null; return { currentUserInfo, preRequestUserState, keyserverIDs }; }; function useLogOut(): ( keyserverIDs?: $ReadOnlyArray, ) => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; const preRequestUserState = usePreRequestUserState(); const callKeyserverLogOut = useKeyserverCall(keyserverLogOut); const commServicesAccessToken = useSelector( state => state.commServicesAccessToken, ); return React.useCallback( async (keyserverIDs?: $ReadOnlyArray) => { const identityPromise = (async () => { if (!usingCommServicesAccessToken) { return; } invariant(identityClient, 'Identity client should be set'); try { await Promise.race([ identityClient.logOut(), (async () => { await sleep(500); throw new Error('identity log_out took more than 500ms'); })(), ]); } catch {} })(); const [{ keyserverIDs: _, ...result }] = await Promise.all([ callKeyserverLogOut({ preRequestUserState, keyserverIDs, }), identityPromise, ]); return { ...result, preRequestUserState: { ...result.preRequestUserState, commServicesAccessToken, }, }; }, [ callKeyserverLogOut, commServicesAccessToken, identityClient, preRequestUserState, ], ); } const claimUsernameActionTypes = Object.freeze({ started: 'CLAIM_USERNAME_STARTED', success: 'CLAIM_USERNAME_SUCCESS', failed: 'CLAIM_USERNAME_FAILED', }); const claimUsernameCallSingleKeyserverEndpointOptions = { timeout: 500 }; const claimUsername = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (() => Promise) => async () => { const requests = { [authoritativeKeyserverID()]: {} }; const responses = await callKeyserverEndpoint('claim_username', requests, { ...claimUsernameCallSingleKeyserverEndpointOptions, }); const response = responses[authoritativeKeyserverID()]; return { message: response.message, signature: response.signature, }; }; function useClaimUsername(): () => Promise { return useKeyserverCall(claimUsername); } const deleteKeyserverAccountActionTypes = Object.freeze({ started: 'DELETE_KEYSERVER_ACCOUNT_STARTED', success: 'DELETE_KEYSERVER_ACCOUNT_SUCCESS', failed: 'DELETE_KEYSERVER_ACCOUNT_FAILED', }); const deleteKeyserverAccount = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): ((input: KeyserverLogOutInput) => Promise) => async input => { const { preRequestUserState } = input; const keyserverIDs = input.keyserverIDs ?? allKeyserverIDs; const requests: { [string]: {} } = {}; for (const keyserverID of keyserverIDs) { requests[keyserverID] = {}; } await callKeyserverEndpoint('delete_account', requests); return { currentUserInfo: loggedOutUserInfo, preRequestUserState, keyserverIDs, }; }; function useDeleteKeyserverAccount(): ( keyserverIDs?: $ReadOnlyArray, ) => Promise { const preRequestUserState = usePreRequestUserState(); const callKeyserverDeleteAccount = useKeyserverCall(deleteKeyserverAccount); return React.useCallback( (keyserverIDs?: $ReadOnlyArray) => callKeyserverDeleteAccount({ preRequestUserState, keyserverIDs }), [callKeyserverDeleteAccount, preRequestUserState], ); } const deleteAccountActionTypes = Object.freeze({ started: 'DELETE_ACCOUNT_STARTED', success: 'DELETE_ACCOUNT_SUCCESS', failed: 'DELETE_ACCOUNT_FAILED', }); function useDeleteAccount(): () => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; const preRequestUserState = usePreRequestUserState(); const callKeyserverDeleteAccount = useKeyserverCall(deleteKeyserverAccount); const commServicesAccessToken = useSelector( state => state.commServicesAccessToken, ); return React.useCallback(async () => { const identityPromise = (async () => { if (!usingCommServicesAccessToken) { return undefined; } if (!identityClient) { throw new Error('Identity service client is not initialized'); } if (!identityClient.deleteWalletUser) { throw new Error('Delete wallet user method unimplemented'); } return await identityClient.deleteWalletUser(); })(); const [keyserverResult] = await Promise.all([ callKeyserverDeleteAccount({ preRequestUserState, }), identityPromise, ]); const { keyserverIDs: _, ...result } = keyserverResult; return { ...result, preRequestUserState: { ...result.preRequestUserState, commServicesAccessToken, }, }; }, [ callKeyserverDeleteAccount, commServicesAccessToken, identityClient, preRequestUserState, ]); } const legacyKeyserverRegisterActionTypes = Object.freeze({ started: 'LEGACY_KEYSERVER_REGISTER_STARTED', success: 'LEGACY_KEYSERVER_REGISTER_SUCCESS', failed: 'LEGACY_KEYSERVER_REGISTER_FAILED', }); const legacyKeyserverRegisterCallSingleKeyserverEndpointOptions = { timeout: permissionsAndAuthRelatedRequestTimeout, }; const legacyKeyserverRegister = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): (( registerInfo: LegacyRegisterInfo, options?: CallSingleKeyserverEndpointOptions, ) => Promise) => async (registerInfo, options) => { const deviceTokenUpdateRequest = registerInfo.deviceTokenUpdateRequest[authoritativeKeyserverID()]; const { preRequestUserInfo, ...rest } = registerInfo; const response = await callSingleKeyserverEndpoint( 'create_account', { ...rest, deviceTokenUpdateRequest, platformDetails: getConfig().platformDetails, }, { ...legacyKeyserverRegisterCallSingleKeyserverEndpointOptions, ...options, }, ); return { currentUserInfo: response.currentUserInfo, rawMessageInfos: response.rawMessageInfos, threadInfos: response.cookieChange.threadInfos, userInfos: response.cookieChange.userInfos, calendarQuery: registerInfo.calendarQuery, }; }; export type KeyserverAuthInput = $ReadOnly<{ ...KeyserverAuthInfo, +preRequestUserInfo: ?CurrentUserInfo, }>; const keyserverAuthActionTypes = Object.freeze({ started: 'KEYSERVER_AUTH_STARTED', success: 'KEYSERVER_AUTH_SUCCESS', failed: 'KEYSERVER_AUTH_FAILED', }); const keyserverAuthCallSingleKeyserverEndpointOptions = { timeout: permissionsAndAuthRelatedRequestTimeout, }; const keyserverAuth = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): ((input: KeyserverAuthInput) => Promise) => async keyserverAuthInfo => { const watchedIDs = threadWatcher.getWatchedIDs(); const { authActionSource, calendarQuery, keyserverData, deviceTokenUpdateInput, preRequestUserInfo, ...restLogInInfo } = keyserverAuthInfo; const keyserverIDs = Object.keys(keyserverData); const watchedIDsPerKeyserver = sortThreadIDsPerKeyserver(watchedIDs); const calendarQueryPerKeyserver = sortCalendarQueryPerKeyserver( calendarQuery, keyserverIDs, ); const requests: { [string]: KeyserverAuthRequest } = {}; for (const keyserverID of keyserverIDs) { requests[keyserverID] = { ...restLogInInfo, deviceTokenUpdateRequest: deviceTokenUpdateInput[keyserverID], watchedIDs: watchedIDsPerKeyserver[keyserverID] ?? [], calendarQuery: calendarQueryPerKeyserver[keyserverID], platformDetails: getConfig().platformDetails, initialContentEncryptedMessage: keyserverData[keyserverID].initialContentEncryptedMessage, initialNotificationsEncryptedMessage: keyserverData[keyserverID].initialNotificationsEncryptedMessage, source: authActionSource, }; } const responses: { +[string]: ClientLogInResponse } = await callKeyserverEndpoint( 'keyserver_auth', requests, keyserverAuthCallSingleKeyserverEndpointOptions, ); const userInfosArrays = []; let threadInfos: RawThreadInfos = {}; const calendarResult: WritableCalendarResult = { calendarQuery: keyserverAuthInfo.calendarQuery, rawEntryInfos: [], }; const messagesResult: WritableGenericMessagesResult = { messageInfos: [], truncationStatus: {}, watchedIDsAtRequestTime: watchedIDs, currentAsOf: {}, }; let updatesCurrentAsOf: { +[string]: number } = {}; for (const keyserverID in responses) { threadInfos = { ...responses[keyserverID].cookieChange.threadInfos, ...threadInfos, }; if (responses[keyserverID].rawEntryInfos) { calendarResult.rawEntryInfos = calendarResult.rawEntryInfos.concat( responses[keyserverID].rawEntryInfos, ); } messagesResult.messageInfos = messagesResult.messageInfos.concat( responses[keyserverID].rawMessageInfos, ); messagesResult.truncationStatus = { ...messagesResult.truncationStatus, ...responses[keyserverID].truncationStatuses, }; messagesResult.currentAsOf = { ...messagesResult.currentAsOf, [keyserverID]: responses[keyserverID].serverTime, }; updatesCurrentAsOf = { ...updatesCurrentAsOf, [keyserverID]: responses[keyserverID].serverTime, }; userInfosArrays.push(responses[keyserverID].userInfos); userInfosArrays.push(responses[keyserverID].cookieChange.userInfos); } const userInfos = mergeUserInfos(...userInfosArrays); return { threadInfos, currentUserInfo: responses[authoritativeKeyserverID()]?.currentUserInfo, calendarResult, messagesResult, userInfos, updatesCurrentAsOf, authActionSource: keyserverAuthInfo.authActionSource, notAcknowledgedPolicies: responses[authoritativeKeyserverID()]?.notAcknowledgedPolicies, preRequestUserInfo, }; }; const identityRegisterActionTypes = Object.freeze({ started: 'IDENTITY_REGISTER_STARTED', success: 'IDENTITY_REGISTER_SUCCESS', failed: 'IDENTITY_REGISTER_FAILED', }); function useIdentityPasswordRegister(): ( username: string, password: string, fid: ?string, ) => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; invariant(identityClient, 'Identity client should be set'); if (!identityClient.registerPasswordUser) { throw new Error('Register password user method unimplemented'); } return identityClient.registerPasswordUser; } function useIdentityWalletRegister(): ( walletAddress: string, siweMessage: string, siweSignature: string, fid: ?string, ) => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; invariant(identityClient, 'Identity client should be set'); if (!identityClient.registerWalletUser) { throw new Error('Register wallet user method unimplemented'); } return identityClient.registerWalletUser; } const identityGenerateNonceActionTypes = Object.freeze({ started: 'IDENTITY_GENERATE_NONCE_STARTED', success: 'IDENTITY_GENERATE_NONCE_SUCCESS', failed: 'IDENTITY_GENERATE_NONCE_FAILED', }); function useIdentityGenerateNonce(): () => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; invariant(identityClient, 'Identity client should be set'); return identityClient.generateNonce; } function mergeUserInfos( ...userInfoArrays: Array<$ReadOnlyArray> ): UserInfo[] { const merged: { [string]: UserInfo } = {}; for (const userInfoArray of userInfoArrays) { for (const userInfo of userInfoArray) { merged[userInfo.id] = userInfo; } } const flattened = []; for (const id in merged) { flattened.push(merged[id]); } return flattened; } type WritableGenericMessagesResult = { messageInfos: RawMessageInfo[], truncationStatus: MessageTruncationStatuses, watchedIDsAtRequestTime: string[], currentAsOf: { [keyserverID: string]: number }, }; type WritableCalendarResult = { rawEntryInfos: RawEntryInfo[], calendarQuery: CalendarQuery, }; const identityLogInActionTypes = Object.freeze({ started: 'IDENTITY_LOG_IN_STARTED', success: 'IDENTITY_LOG_IN_SUCCESS', failed: 'IDENTITY_LOG_IN_FAILED', }); function useIdentityPasswordLogIn(): ( username: string, password: string, ) => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; const preRequestUserState = useSelector(state => state.currentUserInfo); return React.useCallback( (username, password) => { if (!identityClient) { throw new Error('Identity service client is not initialized'); } return (async () => { const result = await identityClient.logInPasswordUser( username, password, ); return { ...result, preRequestUserState, }; })(); }, [identityClient, preRequestUserState], ); } function useIdentityWalletLogIn(): ( walletAddress: string, siweMessage: string, siweSignature: string, ) => Promise { const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; invariant(identityClient, 'Identity client should be set'); return identityClient.logInWalletUser; } const legacyLogInActionTypes = Object.freeze({ started: 'LEGACY_LOG_IN_STARTED', success: 'LEGACY_LOG_IN_SUCCESS', failed: 'LEGACY_LOG_IN_FAILED', }); const legacyLogInCallSingleKeyserverEndpointOptions = { timeout: permissionsAndAuthRelatedRequestTimeout, }; const legacyLogIn = ( callKeyserverEndpoint: CallKeyserverEndpoint, - ): ((input: LogInInfo) => Promise) => + ): ((input: LegacyLogInInfo) => Promise) => async logInInfo => { const watchedIDs = threadWatcher.getWatchedIDs(); const { authActionSource, calendarQuery, keyserverIDs: inputKeyserverIDs, preRequestUserInfo, ...restLogInInfo } = logInInfo; // Eventually the list of keyservers will be fetched from the // identity service const keyserverIDs = inputKeyserverIDs ?? [authoritativeKeyserverID()]; const watchedIDsPerKeyserver = sortThreadIDsPerKeyserver(watchedIDs); const calendarQueryPerKeyserver = sortCalendarQueryPerKeyserver( calendarQuery, keyserverIDs, ); const requests: { [string]: LogInRequest } = {}; for (const keyserverID of keyserverIDs) { requests[keyserverID] = { ...restLogInInfo, deviceTokenUpdateRequest: logInInfo.deviceTokenUpdateRequest[keyserverID], source: authActionSource, watchedIDs: watchedIDsPerKeyserver[keyserverID] ?? [], calendarQuery: calendarQueryPerKeyserver[keyserverID], platformDetails: getConfig().platformDetails, }; } const responses: { +[string]: ClientLogInResponse } = await callKeyserverEndpoint( 'log_in', requests, legacyLogInCallSingleKeyserverEndpointOptions, ); const userInfosArrays = []; let threadInfos: RawThreadInfos = {}; const calendarResult: WritableCalendarResult = { calendarQuery: logInInfo.calendarQuery, rawEntryInfos: [], }; const messagesResult: WritableGenericMessagesResult = { messageInfos: [], truncationStatus: {}, watchedIDsAtRequestTime: watchedIDs, currentAsOf: {}, }; let updatesCurrentAsOf: { +[string]: number } = {}; for (const keyserverID in responses) { threadInfos = { ...responses[keyserverID].cookieChange.threadInfos, ...threadInfos, }; if (responses[keyserverID].rawEntryInfos) { calendarResult.rawEntryInfos = calendarResult.rawEntryInfos.concat( responses[keyserverID].rawEntryInfos, ); } messagesResult.messageInfos = messagesResult.messageInfos.concat( responses[keyserverID].rawMessageInfos, ); messagesResult.truncationStatus = { ...messagesResult.truncationStatus, ...responses[keyserverID].truncationStatuses, }; messagesResult.currentAsOf = { ...messagesResult.currentAsOf, [keyserverID]: responses[keyserverID].serverTime, }; updatesCurrentAsOf = { ...updatesCurrentAsOf, [keyserverID]: responses[keyserverID].serverTime, }; userInfosArrays.push(responses[keyserverID].userInfos); userInfosArrays.push(responses[keyserverID].cookieChange.userInfos); } const userInfos = mergeUserInfos(...userInfosArrays); return { threadInfos, currentUserInfo: responses[authoritativeKeyserverID()].currentUserInfo, calendarResult, messagesResult, userInfos, updatesCurrentAsOf, authActionSource: logInInfo.authActionSource, notAcknowledgedPolicies: responses[authoritativeKeyserverID()].notAcknowledgedPolicies, preRequestUserInfo, }; }; -function useLegacyLogIn(): (input: LogInInfo) => Promise { +function useLegacyLogIn(): ( + input: LegacyLogInInfo, +) => Promise { return useKeyserverCall(legacyLogIn); } const changeKeyserverUserPasswordActionTypes = Object.freeze({ started: 'CHANGE_KEYSERVER_USER_PASSWORD_STARTED', success: 'CHANGE_KEYSERVER_USER_PASSWORD_SUCCESS', failed: 'CHANGE_KEYSERVER_USER_PASSWORD_FAILED', }); const changeKeyserverUserPassword = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): ((passwordUpdate: PasswordUpdate) => Promise) => async passwordUpdate => { await callSingleKeyserverEndpoint('update_account', passwordUpdate); }; const searchUsersActionTypes = Object.freeze({ started: 'SEARCH_USERS_STARTED', success: 'SEARCH_USERS_SUCCESS', failed: 'SEARCH_USERS_FAILED', }); const searchUsers = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): ((usernamePrefix: string) => Promise) => async usernamePrefix => { const response = await callSingleKeyserverEndpoint('search_users', { prefix: usernamePrefix, }); return { userInfos: response.userInfos, }; }; const exactSearchUserActionTypes = Object.freeze({ started: 'EXACT_SEARCH_USER_STARTED', success: 'EXACT_SEARCH_USER_SUCCESS', failed: 'EXACT_SEARCH_USER_FAILED', }); const exactSearchUser = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): ((username: string) => Promise) => async username => { const response = await callSingleKeyserverEndpoint('exact_search_user', { username, }); return { userInfo: response.userInfo, }; }; const updateSubscriptionActionTypes = Object.freeze({ started: 'UPDATE_SUBSCRIPTION_STARTED', success: 'UPDATE_SUBSCRIPTION_SUCCESS', failed: 'UPDATE_SUBSCRIPTION_FAILED', }); const updateSubscription = ( callKeyserverEndpoint: CallKeyserverEndpoint, ): (( input: SubscriptionUpdateRequest, ) => Promise) => async input => { const keyserverID = extractKeyserverIDFromID(input.threadID); const requests = { [keyserverID]: input }; const responses = await callKeyserverEndpoint( 'update_user_subscription', requests, ); const response = responses[keyserverID]; return { threadID: input.threadID, subscription: response.threadSubscription, }; }; function useUpdateSubscription(): ( input: SubscriptionUpdateRequest, ) => Promise { return useKeyserverCall(updateSubscription); } const setUserSettingsActionTypes = Object.freeze({ started: 'SET_USER_SETTINGS_STARTED', success: 'SET_USER_SETTINGS_SUCCESS', failed: 'SET_USER_SETTINGS_FAILED', }); const setUserSettings = ( callKeyserverEndpoint: CallKeyserverEndpoint, allKeyserverIDs: $ReadOnlyArray, ): ((input: UpdateUserSettingsRequest) => Promise) => async input => { const requests: { [string]: UpdateUserSettingsRequest } = {}; for (const keyserverID of allKeyserverIDs) { requests[keyserverID] = input; } await callKeyserverEndpoint('update_user_settings', requests); }; function useSetUserSettings(): ( input: UpdateUserSettingsRequest, ) => Promise { return useKeyserverCall(setUserSettings); } const getOlmSessionInitializationDataActionTypes = Object.freeze({ started: 'GET_OLM_SESSION_INITIALIZATION_DATA_STARTED', success: 'GET_OLM_SESSION_INITIALIZATION_DATA_SUCCESS', failed: 'GET_OLM_SESSION_INITIALIZATION_DATA_FAILED', }); const getOlmSessionInitializationData = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): (( options?: ?CallSingleKeyserverEndpointOptions, ) => Promise) => async options => { const olmInitData = await callSingleKeyserverEndpoint( 'get_olm_session_initialization_data', {}, options, ); return { signedIdentityKeysBlob: olmInitData.signedIdentityKeysBlob, contentInitializationInfo: { ...olmInitData.contentInitializationInfo, oneTimeKey: getOneTimeKeyValuesFromBlob( olmInitData.contentInitializationInfo.oneTimeKey, )[0], prekey: getPrekeyValueFromBlob( olmInitData.contentInitializationInfo.prekey, ), }, notifInitializationInfo: { ...olmInitData.notifInitializationInfo, oneTimeKey: getOneTimeKeyValuesFromBlob( olmInitData.notifInitializationInfo.oneTimeKey, )[0], prekey: getPrekeyValueFromBlob( olmInitData.notifInitializationInfo.prekey, ), }, }; }; const policyAcknowledgmentActionTypes = Object.freeze({ started: 'POLICY_ACKNOWLEDGMENT_STARTED', success: 'POLICY_ACKNOWLEDGMENT_SUCCESS', failed: 'POLICY_ACKNOWLEDGMENT_FAILED', }); const policyAcknowledgment = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): ((policyRequest: PolicyAcknowledgmentRequest) => Promise) => async policyRequest => { await callSingleKeyserverEndpoint('policy_acknowledgment', policyRequest); }; const updateUserAvatarActionTypes = Object.freeze({ started: 'UPDATE_USER_AVATAR_STARTED', success: 'UPDATE_USER_AVATAR_SUCCESS', failed: 'UPDATE_USER_AVATAR_FAILED', }); const updateUserAvatar = ( callSingleKeyserverEndpoint: CallSingleKeyserverEndpoint, ): (( avatarDBContent: UpdateUserAvatarRequest, ) => Promise) => async avatarDBContent => { const { updates }: UpdateUserAvatarResponse = await callSingleKeyserverEndpoint('update_user_avatar', avatarDBContent); return { updates }; }; export { changeKeyserverUserPasswordActionTypes, changeKeyserverUserPassword, claimUsernameActionTypes, useClaimUsername, useDeleteKeyserverAccount, deleteKeyserverAccountActionTypes, getOlmSessionInitializationDataActionTypes, getOlmSessionInitializationData, mergeUserInfos, legacyLogIn as legacyLogInRawAction, identityLogInActionTypes, useIdentityPasswordLogIn, useIdentityWalletLogIn, useLegacyLogIn, legacyLogInActionTypes, useLogOut, logOutActionTypes, legacyKeyserverRegister, legacyKeyserverRegisterActionTypes, searchUsers, searchUsersActionTypes, exactSearchUser, exactSearchUserActionTypes, useSetUserSettings, setUserSettingsActionTypes, useUpdateSubscription, updateSubscriptionActionTypes, policyAcknowledgment, policyAcknowledgmentActionTypes, updateUserAvatarActionTypes, updateUserAvatar, deleteAccountActionTypes, useDeleteAccount, keyserverAuthActionTypes, keyserverAuth as keyserverAuthRawAction, identityRegisterActionTypes, useIdentityPasswordRegister, useIdentityWalletRegister, identityGenerateNonceActionTypes, useIdentityGenerateNonce, }; diff --git a/lib/reducers/report-store-reducer.test.js b/lib/reducers/report-store-reducer.test.js index b781878bf..562814d8f 100644 --- a/lib/reducers/report-store-reducer.test.js +++ b/lib/reducers/report-store-reducer.test.js @@ -1,311 +1,311 @@ // @flow import reduceReportStore from './report-store-reducer.js'; -import type { LogInResult } from '../types/account-types.js'; +import type { LegacyLogInResult } from '../types/account-types.js'; import type { LoadingInfo } from '../types/loading-types.js'; import type { AppState, BaseAction } from '../types/redux-types.js'; import { type ReportStore, reportTypes, type ClientMediaMissionReportCreationRequest, type ClientErrorReportCreationRequest, type EnabledReports, type ClientThreadInconsistencyReportCreationRequest, } from '../types/report-types.js'; const loadingInfo: LoadingInfo = { fetchIndex: 0, trackMultipleRequests: false, customKeyName: undefined, }; // this is only for types compatibility and `any` will not have any influence // on tests correctness const defaultState: AppState = ({}: any); const defaultBaseAction: BaseAction = ({ payload: ({}: any), loadingInfo, }: any); const defaultAction = { payload: ({}: any), loadingInfo, }; const mockErrorReport: ClientErrorReportCreationRequest = { type: reportTypes.ERROR, platformDetails: { platform: 'web' }, errors: [], preloadedState: defaultState, currentState: defaultState, actions: [], id: '1-1', }; const mockInconsistencyReport: ClientThreadInconsistencyReportCreationRequest = { type: reportTypes.THREAD_INCONSISTENCY, platformDetails: { platform: 'web' }, beforeAction: {}, action: defaultBaseAction, pushResult: {}, lastActions: [], time: 0, id: '1-2', }; const mockMediaReport: ClientMediaMissionReportCreationRequest = { type: reportTypes.MEDIA_MISSION, platformDetails: { platform: 'web' }, time: Date.now(), mediaMission: { steps: [], result: { success: true }, totalTime: 0, userTime: 0, }, id: '1-3', }; const defaultEnabledReports: EnabledReports = { crashReports: true, inconsistencyReports: true, mediaReports: true, }; const defaultEmptyReportStore: ReportStore = { queuedReports: [], enabledReports: defaultEnabledReports, }; const defaultReportStore: ReportStore = { queuedReports: [mockErrorReport, mockInconsistencyReport, mockMediaReport], enabledReports: defaultEnabledReports, }; describe('session change test', () => { - const mockLogInResult: LogInResult = ({ + const mockLogInResult: LegacyLogInResult = ({ currentUserInfo: { id: '-1', username: 'test' }, }: any); test('should handle log out', () => { const action = { ...defaultAction, type: 'LOG_OUT_SUCCESS' }; const { reportStore: result } = reduceReportStore( defaultReportStore, action, [], ); expect(result.queuedReports).toHaveLength(0); }); test('should handle log out with new inconsistencies', () => { const action = { ...defaultAction, type: 'LOG_OUT_SUCCESS' }; const { reportStore: result } = reduceReportStore( defaultReportStore, action, [mockErrorReport], ); expect(result.queuedReports).toHaveLength(0); }); test('should handle log in', () => { const action = { type: 'LEGACY_LOG_IN_SUCCESS', payload: mockLogInResult, loadingInfo, }; const { reportStore: result } = reduceReportStore( defaultReportStore, action, [], ); expect(result.queuedReports).toHaveLength(0); }); test('should handle log in with new inconsistencies', () => { const action = { type: 'LEGACY_LOG_IN_SUCCESS', payload: mockLogInResult, loadingInfo, }; const { reportStore: result } = reduceReportStore( defaultReportStore, action, [mockErrorReport], ); expect(result.queuedReports).toHaveLength(0); }); }); describe('updateReportsEnabledActionType test', () => { test('should handle the same enabled reports', () => { const action = { type: 'UPDATE_REPORTS_ENABLED', payload: defaultEnabledReports, }; const { reportStore: result } = reduceReportStore( defaultReportStore, action, [], ); expect(result.queuedReports).toStrictEqual( defaultReportStore.queuedReports, ); }); test('should handle changing enabled reports', () => { const action = { type: 'UPDATE_REPORTS_ENABLED', payload: { crashReports: true, inconsistencyReports: false, mediaReports: false, }, }; const { reportStore: result } = reduceReportStore( defaultReportStore, action, [], ); expect(result.queuedReports).toHaveLength(1); const enabledReportsExist = result.queuedReports.some( report => report.type === reportTypes.ERROR, ); const notEnabledReportsExist = result.queuedReports.some( report => report.type !== reportTypes.ERROR, ); expect(enabledReportsExist).toBeTruthy(); expect(notEnabledReportsExist).toBeFalsy(); }); test('should handle changing enabled reports with new inconsistencies', () => { const action = { type: 'UPDATE_REPORTS_ENABLED', payload: { crashReports: true, inconsistencyReports: false, mediaReports: false, }, }; const { reportStore: result } = reduceReportStore( defaultReportStore, action, [ { ...mockErrorReport, id: 'new-id-error' }, { ...mockMediaReport, id: 'new-id-media' }, ], ); expect(result.queuedReports).toHaveLength(2); const enabledReports = result.queuedReports.filter( report => report.type === reportTypes.ERROR, ); const notEnabledReportsExist = result.queuedReports.some( report => report.type !== reportTypes.ERROR, ); expect(enabledReports).toHaveLength(2); expect(notEnabledReportsExist).toBeFalsy(); }); }); describe('queueReportsActionType test', () => { test('should handle adding enabled report', () => { const action = { type: 'QUEUE_REPORTS', payload: { reports: [ { ...mockErrorReport, id: 'new-id-error' }, { ...mockMediaReport, id: 'new-id-media' }, ], }, }; const reportStore = { queuedReports: [mockErrorReport], enabledReports: { crashReports: true, inconsistencyReports: false, mediaReports: false, }, }; const { reportStore: result } = reduceReportStore(reportStore, action, [ { ...mockErrorReport, id: 'new-id-error-inc' }, { ...mockMediaReport, id: 'new-id-media-inc' }, ]); expect(result.queuedReports).toHaveLength(3); const enabledReports = result.queuedReports.filter( report => report.type === reportTypes.ERROR, ); const notEnabledReportsExist = result.queuedReports.some( report => report.type !== reportTypes.ERROR, ); expect(enabledReports).toHaveLength(3); expect(notEnabledReportsExist).toBeFalsy(); }); }); describe('sending report test', () => { test('should remove sent report', () => { const { reportStore } = reduceReportStore( defaultEmptyReportStore, defaultBaseAction, [mockErrorReport, mockMediaReport], ); expect(reportStore.queuedReports).toHaveLength(2); const [sentReport, notSentReport] = reportStore.queuedReports; const action = { type: 'SEND_REPORT_SUCCESS', payload: { reports: [sentReport], }, loadingInfo, }; const { reportStore: result } = reduceReportStore(reportStore, action, []); expect(result.queuedReports).toHaveLength(1); expect(result.queuedReports).toContain(notSentReport); expect(result.queuedReports).not.toContain(sentReport); }); test('should remove sent report and handle new inconsistencies', () => { const { reportStore } = reduceReportStore( defaultEmptyReportStore, defaultBaseAction, [mockErrorReport, mockMediaReport], ); expect(reportStore.queuedReports).toHaveLength(2); const [sentReport, notSentReport] = reportStore.queuedReports; const action = { type: 'SEND_REPORT_SUCCESS', payload: { reports: [sentReport], }, loadingInfo, }; const { reportStore: result } = reduceReportStore(reportStore, action, [ mockInconsistencyReport, ]); expect(result.queuedReports).toHaveLength(2); expect(result.queuedReports).toContain(notSentReport); expect(result.queuedReports).not.toContain(sentReport); }); }); describe('new inconsistencies test', () => { test('should handle new inconsistencies without any action', () => { const { reportStore } = reduceReportStore( { queuedReports: [mockErrorReport], enabledReports: { crashReports: true, inconsistencyReports: false, mediaReports: false, }, }, defaultBaseAction, [ { ...mockErrorReport, id: 'new-id-error' }, { ...mockMediaReport, id: 'new-id-media' }, ], ); expect(reportStore.queuedReports).toHaveLength(2); }); }); diff --git a/lib/types/account-types.js b/lib/types/account-types.js index eafaa02b0..01957a5af 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, +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 LogInStartingPayload = { +export type LegacyLogInStartingPayload = { +calendarQuery: CalendarQuery, +authActionSource?: AuthActionSource, }; export type LogInExtraInfo = { +calendarQuery: CalendarQuery, +deviceTokenUpdateRequest: DeviceTokenUpdateInput, +signedIdentityKeysBlob?: SignedIdentityKeysBlob, +initialNotificationsEncryptedMessage?: string, +preRequestUserInfo: ?CurrentUserInfo, }; -export type LogInInfo = { +export type LegacyLogInInfo = { ...LogInExtraInfo, +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 LogInResult = { +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/redux-types.js b/lib/types/redux-types.js index eb0338312..2365bdcac 100644 --- a/lib/types/redux-types.js +++ b/lib/types/redux-types.js @@ -1,1436 +1,1436 @@ // @flow import type { LogOutResult, KeyserverLogOutResult, - LogInStartingPayload, - LogInResult, + LegacyLogInStartingPayload, + LegacyLogInResult, LegacyRegisterResult, DefaultNotificationPayload, ClaimUsernameResponse, KeyserverAuthResult, } from './account-types.js'; import type { ActivityUpdateSuccessPayload, QueueActivityUpdatesPayload, SetThreadUnreadStatusPayload, } from './activity-types.js'; import type { AlertStore, RecordAlertActionPayload } from './alert-types.js'; import type { AuxUserStore, SetFarcasterFriendsFIDPayload, } from './aux-user-types.js'; import type { UpdateUserAvatarRequest, UpdateUserAvatarResponse, } from './avatar-types.js'; import type { CommunityStore, AddCommunityPayload, CreateOrUpdateFarcasterChannelTagResponse, } from './community-types.js'; import type { MessageSourceMetadata, DBOpsStore } from './db-ops-types.js'; import type { GetVersionActionPayload, LastCommunicatedPlatformDetails, } from './device-types.js'; import type { DraftStore } from './draft-types.js'; import type { EnabledApps, SupportedApps } from './enabled-apps.js'; import type { RawEntryInfo, EntryStore, SaveEntryPayload, CreateEntryPayload, DeleteEntryResult, RestoreEntryPayload, FetchEntryInfosResult, CalendarQueryUpdateResult, CalendarQueryUpdateStartingPayload, FetchRevisionsForEntryPayload, } from './entry-types.js'; import type { CalendarFilter, CalendarThreadFilter, SetCalendarDeletedFilterPayload, } from './filter-types.js'; import type { IdentityAuthResult } from './identity-service-types.js'; import type { IntegrityStore } from './integrity-types.js'; import type { KeyserverStore, AddKeyserverPayload, RemoveKeyserverPayload, } from './keyserver-types.js'; import type { LifecycleState } from './lifecycle-state-types.js'; import type { FetchInviteLinksResponse, InviteLink, InviteLinksStore, InviteLinkVerificationResponse, DisableInviteLinkPayload, } from './link-types.js'; import type { LoadingStatus, LoadingInfo } from './loading-types.js'; import type { UpdateMultimediaMessageMediaPayload } from './media-types.js'; import type { MessageReportCreationResult } from './message-report-types.js'; import type { MessageStore, RawMultimediaMessageInfo, FetchMessageInfosPayload, SendMessagePayload, EditMessagePayload, SaveMessagesPayload, NewMessagesPayload, MessageStorePrunePayload, LocallyComposedMessageInfo, SimpleMessagesPayload, FetchPinnedMessagesResult, SearchMessagesResponse, } from './message-types.js'; import type { RawReactionMessageInfo } from './messages/reaction.js'; import type { RawTextMessageInfo } from './messages/text.js'; import type { BaseNavInfo, WebNavInfo } from './nav-types.js'; import { type ForcePolicyAcknowledgmentPayload, type PolicyAcknowledgmentPayload, type UserPolicies, } from './policy-types.js'; import type { RelationshipErrors } from './relationship-types.js'; import type { EnabledReports, ClearDeliveredReportsPayload, QueueReportsPayload, ReportStore, } from './report-types.js'; import type { ProcessServerRequestAction, GetOlmSessionInitializationDataResponse, } from './request-types.js'; import type { UserSearchResult, ExactUserSearchResult, } from './search-types.js'; import type { SetSessionPayload } from './session-types.js'; import type { ConnectionIssue, StateSyncFullActionPayload, StateSyncIncrementalActionPayload, SetActiveSessionRecoveryPayload, } from './socket-types.js'; import { type ClientStore } from './store-ops-types.js'; import type { SubscriptionUpdateResult } from './subscription-types.js'; import type { SyncedMetadataStore, SetSyncedMetadataEntryPayload, ClearSyncedMetadataEntryPayload, } from './synced-metadata-types.js'; import type { GlobalThemeInfo } from './theme-types.js'; import type { ThreadActivityStore } from './thread-activity-types.js'; import type { ThreadStore, ChangeThreadSettingsPayload, LeaveThreadPayload, NewThreadResult, ThreadJoinPayload, ToggleMessagePinResult, LegacyThreadStore, RoleModificationPayload, RoleDeletionPayload, } from './thread-types.js'; import type { ClientUpdatesResultWithUserInfos } from './update-types.js'; import type { CurrentUserInfo, UserInfos, UserStore } from './user-types.js'; import type { SetDeviceTokenActionPayload } from '../actions/device-actions.js'; import type { UpdateConnectionStatusPayload, SetLateResponsePayload, UpdateKeyserverReachabilityPayload, } from '../keyserver-conn/keyserver-conn-types.js'; export type BaseAppState = { +navInfo: NavInfo, +currentUserInfo: ?CurrentUserInfo, +draftStore: DraftStore, +entryStore: EntryStore, +threadStore: ThreadStore, +userStore: UserStore, +messageStore: MessageStore, +loadingStatuses: { [key: string]: { [idx: number]: LoadingStatus } }, +calendarFilters: $ReadOnlyArray, +alertStore: AlertStore, +watchedThreadIDs: $ReadOnlyArray, +lifecycleState: LifecycleState, +enabledApps: EnabledApps, +reportStore: ReportStore, +dataLoaded: boolean, +userPolicies: UserPolicies, +commServicesAccessToken: ?string, +inviteLinksStore: InviteLinksStore, +keyserverStore: KeyserverStore, +threadActivityStore: ThreadActivityStore, +integrityStore: IntegrityStore, +globalThemeInfo: GlobalThemeInfo, +customServer: ?string, +communityStore: CommunityStore, +dbOpsStore: DBOpsStore, +syncedMetadataStore: SyncedMetadataStore, +auxUserStore: AuxUserStore, ... }; export type NativeAppState = BaseAppState<>; export type WebAppState = BaseAppState<> & { +pushApiPublicKey: ?string, ... }; export type AppState = NativeAppState | WebAppState; export type ClientWebInitialReduxStateResponse = { +navInfo: WebNavInfo, +currentUserInfo: CurrentUserInfo, +entryStore: EntryStore, +threadStore: ThreadStore, +userInfos: UserInfos, +messageStore: MessageStore, +pushApiPublicKey: ?string, +inviteLinksStore: InviteLinksStore, +keyserverInfo: WebInitialKeyserverInfo, }; export type ServerWebInitialReduxStateResponse = { +navInfo: WebNavInfo, +currentUserInfo: CurrentUserInfo, +entryStore: EntryStore, +threadStore: LegacyThreadStore, +userInfos: UserInfos, +messageStore: MessageStore, +pushApiPublicKey: ?string, +inviteLinksStore: InviteLinksStore, +keyserverInfo: WebInitialKeyserverInfo, }; export type WebInitialKeyserverInfo = { +sessionID: ?string, +updatesCurrentAsOf: number, }; export type BaseAction = $ReadOnly<{ +messageSourceMetadata?: MessageSourceMetadata, ... | { +type: '@@redux/INIT', +payload?: void, } | { +type: 'FETCH_ENTRIES_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_ENTRIES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_ENTRIES_SUCCESS', +payload: FetchEntryInfosResult, +loadingInfo: LoadingInfo, } | { +type: 'LOG_OUT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'LOG_OUT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'LOG_OUT_SUCCESS', +payload: LogOutResult, +loadingInfo: LoadingInfo, } | { +type: 'CLAIM_USERNAME_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CLAIM_USERNAME_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CLAIM_USERNAME_SUCCESS', +payload: ClaimUsernameResponse, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_KEYSERVER_ACCOUNT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_KEYSERVER_ACCOUNT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_KEYSERVER_ACCOUNT_SUCCESS', +payload: KeyserverLogOutResult, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_ACCOUNT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_ACCOUNT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_ACCOUNT_SUCCESS', +payload: LogOutResult, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_LOCAL_ENTRY', +payload: RawEntryInfo, } | { +type: 'CREATE_ENTRY_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_ENTRY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_ENTRY_SUCCESS', +payload: CreateEntryPayload, +loadingInfo: LoadingInfo, } | { +type: 'SAVE_ENTRY_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SAVE_ENTRY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SAVE_ENTRY_SUCCESS', +payload: SaveEntryPayload, +loadingInfo: LoadingInfo, } | { +type: 'CONCURRENT_MODIFICATION_RESET', +payload: { +id: string, +dbText: string, }, } | { +type: 'DELETE_ENTRY_STARTED', +loadingInfo: LoadingInfo, +payload: { +localID: ?string, +serverID: ?string, }, } | { +type: 'DELETE_ENTRY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_ENTRY_SUCCESS', +payload: ?DeleteEntryResult, +loadingInfo: LoadingInfo, } | { +type: 'IDENTITY_LOG_IN_STARTED', +loadingInfo: LoadingInfo, +payload?: void, } | { +type: 'IDENTITY_LOG_IN_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'IDENTITY_LOG_IN_SUCCESS', +payload: IdentityAuthResult, +loadingInfo: LoadingInfo, } | { +type: 'KEYSERVER_AUTH_STARTED', +loadingInfo: LoadingInfo, +payload?: void, } | { +type: 'KEYSERVER_AUTH_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'KEYSERVER_AUTH_SUCCESS', +payload: KeyserverAuthResult, +loadingInfo: LoadingInfo, } | { +type: 'LEGACY_LOG_IN_STARTED', +loadingInfo: LoadingInfo, - +payload: LogInStartingPayload, + +payload: LegacyLogInStartingPayload, } | { +type: 'LEGACY_LOG_IN_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'LEGACY_LOG_IN_SUCCESS', - +payload: LogInResult, + +payload: LegacyLogInResult, +loadingInfo: LoadingInfo, } | { +type: 'LEGACY_KEYSERVER_REGISTER_STARTED', +loadingInfo: LoadingInfo, - +payload: LogInStartingPayload, + +payload: LegacyLogInStartingPayload, } | { +type: 'LEGACY_KEYSERVER_REGISTER_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'LEGACY_KEYSERVER_REGISTER_SUCCESS', +payload: LegacyRegisterResult, +loadingInfo: LoadingInfo, } | { +type: 'IDENTITY_REGISTER_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'IDENTITY_REGISTER_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'IDENTITY_REGISTER_SUCCESS', +payload: IdentityAuthResult, +loadingInfo: LoadingInfo, } | { +type: 'IDENTITY_GENERATE_NONCE_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'IDENTITY_GENERATE_NONCE_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'IDENTITY_GENERATE_NONCE_SUCCESS', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_KEYSERVER_USER_PASSWORD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_KEYSERVER_USER_PASSWORD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_KEYSERVER_USER_PASSWORD_SUCCESS', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_THREAD_SETTINGS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_THREAD_SETTINGS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_THREAD_SETTINGS_SUCCESS', +payload: ChangeThreadSettingsPayload, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_THREAD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_THREAD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_THREAD_SUCCESS', +payload: LeaveThreadPayload, +loadingInfo: LoadingInfo, } | { +type: 'NEW_THREAD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'NEW_THREAD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'NEW_THREAD_SUCCESS', +payload: NewThreadResult, +loadingInfo: LoadingInfo, } | { +type: 'REMOVE_USERS_FROM_THREAD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'REMOVE_USERS_FROM_THREAD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'REMOVE_USERS_FROM_THREAD_SUCCESS', +payload: ChangeThreadSettingsPayload, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_THREAD_MEMBER_ROLES_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_THREAD_MEMBER_ROLES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_THREAD_MEMBER_ROLES_SUCCESS', +payload: ChangeThreadSettingsPayload, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_REVISIONS_FOR_ENTRY_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_REVISIONS_FOR_ENTRY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_REVISIONS_FOR_ENTRY_SUCCESS', +payload: FetchRevisionsForEntryPayload, +loadingInfo: LoadingInfo, } | { +type: 'RESTORE_ENTRY_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'RESTORE_ENTRY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'RESTORE_ENTRY_SUCCESS', +payload: RestoreEntryPayload, +loadingInfo: LoadingInfo, } | { +type: 'JOIN_THREAD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'JOIN_THREAD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'JOIN_THREAD_SUCCESS', +payload: ThreadJoinPayload, +loadingInfo: LoadingInfo, } | { +type: 'LEAVE_THREAD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'LEAVE_THREAD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'LEAVE_THREAD_SUCCESS', +payload: LeaveThreadPayload, +loadingInfo: LoadingInfo, } | { +type: 'SET_NEW_SESSION', +payload: SetSessionPayload, } | { +type: 'persist/REHYDRATE', +payload: ?BaseAppState<>, } | { +type: 'FETCH_MESSAGES_BEFORE_CURSOR_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_MESSAGES_BEFORE_CURSOR_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_MESSAGES_BEFORE_CURSOR_SUCCESS', +payload: FetchMessageInfosPayload, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_MOST_RECENT_MESSAGES_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_MOST_RECENT_MESSAGES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_MOST_RECENT_MESSAGES_SUCCESS', +payload: FetchMessageInfosPayload, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_SINGLE_MOST_RECENT_MESSAGES_FROM_THREADS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_SINGLE_MOST_RECENT_MESSAGES_FROM_THREADS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_SINGLE_MOST_RECENT_MESSAGES_FROM_THREADS_SUCCESS', +payload: SimpleMessagesPayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_TEXT_MESSAGE_STARTED', +loadingInfo?: LoadingInfo, +payload: RawTextMessageInfo, } | { +type: 'SEND_TEXT_MESSAGE_FAILED', +error: true, +payload: Error & { +localID: string, +threadID: string, }, +loadingInfo?: LoadingInfo, } | { +type: 'SEND_TEXT_MESSAGE_SUCCESS', +payload: SendMessagePayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_MULTIMEDIA_MESSAGE_STARTED', +loadingInfo?: LoadingInfo, +payload: RawMultimediaMessageInfo, } | { +type: 'SEND_MULTIMEDIA_MESSAGE_FAILED', +error: true, +payload: Error & { +localID: string, +threadID: string, }, +loadingInfo?: LoadingInfo, } | { +type: 'SEND_MULTIMEDIA_MESSAGE_SUCCESS', +payload: SendMessagePayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REACTION_MESSAGE_STARTED', +loadingInfo?: LoadingInfo, +payload: RawReactionMessageInfo, } | { +type: 'SEND_REACTION_MESSAGE_FAILED', +error: true, +payload: Error & { +localID: string, +threadID: string, +targetMessageID: string, +reaction: string, +action: string, }, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REACTION_MESSAGE_SUCCESS', +payload: SendMessagePayload, +loadingInfo: LoadingInfo, } | { +type: 'SEARCH_USERS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SEARCH_USERS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SEARCH_USERS_SUCCESS', +payload: UserSearchResult, +loadingInfo: LoadingInfo, } | { +type: 'EXACT_SEARCH_USER_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'EXACT_SEARCH_USER_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'EXACT_SEARCH_USER_SUCCESS', +payload: ExactUserSearchResult, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_DRAFT', +payload: { +key: string, +text: string, }, } | { +type: 'MOVE_DRAFT', +payload: { +oldKey: string, +newKey: string, }, } | { +type: 'SET_CLIENT_DB_STORE', +payload: ClientStore, } | { +type: 'UPDATE_ACTIVITY_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_ACTIVITY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_ACTIVITY_SUCCESS', +payload: ActivityUpdateSuccessPayload, +loadingInfo: LoadingInfo, } | { +type: 'SET_DEVICE_TOKEN_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SET_DEVICE_TOKEN_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SET_DEVICE_TOKEN_SUCCESS', +payload: SetDeviceTokenActionPayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORT_SUCCESS', +payload?: ClearDeliveredReportsPayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORTS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORTS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORTS_SUCCESS', +payload?: ClearDeliveredReportsPayload, +loadingInfo: LoadingInfo, } | { +type: 'QUEUE_REPORTS', +payload: QueueReportsPayload, } | { +type: 'SET_URL_PREFIX', +payload: string, } | { +type: 'SAVE_MESSAGES', +payload: SaveMessagesPayload, } | { +type: 'UPDATE_CALENDAR_THREAD_FILTER', +payload: CalendarThreadFilter, } | { +type: 'CLEAR_CALENDAR_THREAD_FILTER', +payload?: void, } | { +type: 'SET_CALENDAR_DELETED_FILTER', +payload: SetCalendarDeletedFilterPayload, } | { +type: 'UPDATE_SUBSCRIPTION_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_SUBSCRIPTION_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_SUBSCRIPTION_SUCCESS', +payload: SubscriptionUpdateResult, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_CALENDAR_QUERY_STARTED', +loadingInfo: LoadingInfo, +payload?: CalendarQueryUpdateStartingPayload, } | { +type: 'UPDATE_CALENDAR_QUERY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_CALENDAR_QUERY_SUCCESS', +payload: CalendarQueryUpdateResult, +loadingInfo: LoadingInfo, } | { +type: 'FULL_STATE_SYNC', +payload: StateSyncFullActionPayload, } | { +type: 'INCREMENTAL_STATE_SYNC', +payload: StateSyncIncrementalActionPayload, } | ProcessServerRequestAction | { +type: 'UPDATE_CONNECTION_STATUS', +payload: UpdateConnectionStatusPayload, } | { +type: 'QUEUE_ACTIVITY_UPDATES', +payload: QueueActivityUpdatesPayload, } | { +type: 'UNSUPERVISED_BACKGROUND', +payload: { +keyserverID: string }, } | { +type: 'UPDATE_LIFECYCLE_STATE', +payload: LifecycleState, } | { +type: 'ENABLE_APP', +payload: SupportedApps, } | { +type: 'DISABLE_APP', +payload: SupportedApps, } | { +type: 'UPDATE_REPORTS_ENABLED', +payload: Partial, } | { +type: 'PROCESS_UPDATES', +payload: ClientUpdatesResultWithUserInfos, } | { +type: 'PROCESS_MESSAGES', +payload: NewMessagesPayload, } | { +type: 'MESSAGE_STORE_PRUNE', +payload: MessageStorePrunePayload, } | { +type: 'SET_LATE_RESPONSE', +payload: SetLateResponsePayload, } | { +type: 'UPDATE_KEYSERVER_REACHABILITY', +payload: UpdateKeyserverReachabilityPayload, } | { +type: 'REQUEST_ACCESS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'REQUEST_ACCESS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'REQUEST_ACCESS_SUCCESS', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_MULTIMEDIA_MESSAGE_MEDIA', +payload: UpdateMultimediaMessageMediaPayload, } | { +type: 'CREATE_LOCAL_MESSAGE', +payload: LocallyComposedMessageInfo, } | { +type: 'UPDATE_RELATIONSHIPS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_RELATIONSHIPS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_RELATIONSHIPS_SUCCESS', +payload: RelationshipErrors, +loadingInfo: LoadingInfo, } | { +type: 'SET_THREAD_UNREAD_STATUS_STARTED', +payload: { +threadID: string, +unread: boolean, }, +loadingInfo: LoadingInfo, } | { +type: 'SET_THREAD_UNREAD_STATUS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SET_THREAD_UNREAD_STATUS_SUCCESS', +payload: SetThreadUnreadStatusPayload, } | { +type: 'SET_USER_SETTINGS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SET_USER_SETTINGS_SUCCESS', +payload: DefaultNotificationPayload, } | { +type: 'SET_USER_SETTINGS_FAILED', +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SEND_MESSAGE_REPORT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SEND_MESSAGE_REPORT_SUCCESS', +payload: MessageReportCreationResult, +loadingInfo: LoadingInfo, } | { +type: 'SEND_MESSAGE_REPORT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FORCE_POLICY_ACKNOWLEDGMENT', +payload: ForcePolicyAcknowledgmentPayload, +loadingInfo: LoadingInfo, } | { +type: 'POLICY_ACKNOWLEDGMENT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'POLICY_ACKNOWLEDGMENT_SUCCESS', +payload: PolicyAcknowledgmentPayload, +loadingInfo: LoadingInfo, } | { +type: 'POLICY_ACKNOWLEDGMENT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'GET_SIWE_NONCE_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'GET_SIWE_NONCE_SUCCESS', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'GET_SIWE_NONCE_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'LEGACY_SIWE_AUTH_STARTED', - +payload: LogInStartingPayload, + +payload: LegacyLogInStartingPayload, +loadingInfo: LoadingInfo, } | { +type: 'LEGACY_SIWE_AUTH_SUCCESS', - +payload: LogInResult, + +payload: LegacyLogInResult, +loadingInfo: LoadingInfo, } | { +type: 'LEGACY_SIWE_AUTH_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'RECORD_ALERT', +payload: RecordAlertActionPayload, } | { +type: 'UPDATE_USER_AVATAR_STARTED', +payload: UpdateUserAvatarRequest, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_USER_AVATAR_SUCCESS', +payload: UpdateUserAvatarResponse, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_USER_AVATAR_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SEND_EDIT_MESSAGE_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'SEND_EDIT_MESSAGE_SUCCESS', +payload: EditMessagePayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_EDIT_MESSAGE_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'TOGGLE_MESSAGE_PIN_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'TOGGLE_MESSAGE_PIN_SUCCESS', +payload: ToggleMessagePinResult, +loadingInfo: LoadingInfo, } | { +type: 'TOGGLE_MESSAGE_PIN_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PINNED_MESSAGES_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'FETCH_PINNED_MESSAGES_SUCCESS', +payload: FetchPinnedMessagesResult, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PINNED_MESSAGES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'VERIFY_INVITE_LINK_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'VERIFY_INVITE_LINK_SUCCESS', +payload: InviteLinkVerificationResponse, +loadingInfo: LoadingInfo, } | { +type: 'VERIFY_INVITE_LINK_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PRIMARY_INVITE_LINKS_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'FETCH_PRIMARY_INVITE_LINKS_SUCCESS', +payload: FetchInviteLinksResponse, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PRIMARY_INVITE_LINKS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_CALENDAR_COMMUNITY_FILTER', +payload: string, } | { +type: 'CLEAR_CALENDAR_COMMUNITY_FILTER', +payload: void, } | { +type: 'UPDATE_CHAT_COMMUNITY_FILTER', +payload: string, } | { +type: 'CLEAR_CHAT_COMMUNITY_FILTER', +payload: void, } | { +type: 'SEARCH_MESSAGES_STARTED', +payload: void, +loadingInfo?: LoadingInfo, } | { +type: 'SEARCH_MESSAGES_SUCCESS', +payload: SearchMessagesResponse, +loadingInfo: LoadingInfo, } | { +type: 'SEARCH_MESSAGES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_OR_UPDATE_PUBLIC_LINK_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'CREATE_OR_UPDATE_PUBLIC_LINK_SUCCESS', +payload: InviteLink, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_OR_UPDATE_PUBLIC_LINK_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DISABLE_INVITE_LINK_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'DISABLE_INVITE_LINK_SUCCESS', +payload: DisableInviteLinkPayload, +loadingInfo: LoadingInfo, } | { +type: 'DISABLE_INVITE_LINK_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'GET_OLM_SESSION_INITIALIZATION_DATA_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'GET_OLM_SESSION_INITIALIZATION_DATA_SUCCESS', +payload: GetOlmSessionInitializationDataResponse, +loadingInfo: LoadingInfo, } | { +type: 'GET_OLM_SESSION_INITIALIZATION_DATA_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SET_DATA_LOADED', +payload: { +dataLoaded: boolean, }, } | { +type: 'GET_VERSION_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'GET_VERSION_SUCCESS', +payload: GetVersionActionPayload, +loadingInfo: LoadingInfo, } | { +type: 'GET_VERSION_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_LAST_COMMUNICATED_PLATFORM_DETAILS', +payload: LastCommunicatedPlatformDetails, } | { +type: 'RESET_USER_STATE', +payload?: void } | { +type: 'MODIFY_COMMUNITY_ROLE_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'MODIFY_COMMUNITY_ROLE_SUCCESS', +payload: RoleModificationPayload, +loadingInfo: LoadingInfo, } | { +type: 'MODIFY_COMMUNITY_ROLE_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_COMMUNITY_ROLE_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'DELETE_COMMUNITY_ROLE_SUCCESS', +payload: RoleDeletionPayload, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_COMMUNITY_ROLE_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_THREAD_LAST_NAVIGATED', +payload: { +threadID: string, +time: number }, } | { +type: 'UPDATE_INTEGRITY_STORE', +payload: { +threadIDsToHash?: $ReadOnlyArray, +threadHashingStatus?: 'starting' | 'running' | 'completed', }, } | { +type: 'UPDATE_THEME_INFO', +payload: Partial, } | { +type: 'ADD_KEYSERVER', +payload: AddKeyserverPayload, } | { +type: 'REMOVE_KEYSERVER', +payload: RemoveKeyserverPayload, } | { +type: 'SET_CUSTOM_SERVER', +payload: string, } | { +type: 'SET_CONNECTION_ISSUE', +payload: { +connectionIssue: ?ConnectionIssue, +keyserverID: string }, } | { +type: 'ADD_COMMUNITY', +payload: AddCommunityPayload, } | { +type: 'SET_SYNCED_METADATA_ENTRY', +payload: SetSyncedMetadataEntryPayload, } | { +type: 'CLEAR_SYNCED_METADATA_ENTRY', +payload: ClearSyncedMetadataEntryPayload, } | { +type: 'SET_ACTIVE_SESSION_RECOVERY', +payload: SetActiveSessionRecoveryPayload, } | { +type: 'SET_FARCASTER_FRIENDS_FID', +payload: SetFarcasterFriendsFIDPayload, } | { +type: 'CLEAR_AUX_USER_FIDS', +payload?: void, } | { +type: 'OPS_PROCESSING_FINISHED_ACTION_TYPE', +payload?: void, } | { +type: 'CREATE_OR_UPDATE_FARCASTER_CHANNEL_TAG_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'CREATE_OR_UPDATE_FARCASTER_CHANNEL_TAG_SUCCESS', +payload: CreateOrUpdateFarcasterChannelTagResponse, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_OR_UPDATE_FARCASTER_CHANNEL_TAG_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, }, }>; export type ActionPayload = ?(Object | Array<*> | $ReadOnlyArray<*> | string); export type DispatchSource = 'tunnelbroker' | 'tab-sync'; export type SuperAction = { +type: string, +payload?: ActionPayload, +loadingInfo?: LoadingInfo, +error?: boolean, +dispatchSource?: DispatchSource, +messageSourceMetadata?: MessageSourceMetadata, }; type ThunkedAction = (dispatch: Dispatch) => void; export type PromisedAction = (dispatch: Dispatch) => Promise; export type Dispatch = ((promisedAction: PromisedAction) => Promise) & ((thunkedAction: ThunkedAction) => void) & ((action: SuperAction) => boolean); // This is lifted from redux-persist/lib/constants.js // I don't want to add redux-persist to the web/server bundles... // import { REHYDRATE } from 'redux-persist'; export const rehydrateActionType = 'persist/REHYDRATE'; diff --git a/native/account/legacy-register-panel.react.js b/native/account/legacy-register-panel.react.js index 1f9e94dd0..bdd358139 100644 --- a/native/account/legacy-register-panel.react.js +++ b/native/account/legacy-register-panel.react.js @@ -1,517 +1,519 @@ // @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, LegacyRegisterResult, - LogInStartingPayload, + 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 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, +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 initialNotificationsEncryptedMessage = await this.props.getInitialNotificationsEncryptedMessage(); void this.props.dispatchActionPromise( legacyKeyserverRegisterActionTypes, this.legacyRegisterAction({ ...extraInfo, initialNotificationsEncryptedMessage, }), undefined, - ({ calendarQuery: extraInfo.calendarQuery }: LogInStartingPayload), + ({ + 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, ): 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 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 9326ca8f5..a4a99cd81 100644 --- a/native/account/log-in-panel.react.js +++ b/native/account/log-in-panel.react.js @@ -1,477 +1,479 @@ // @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 LogInInfo, + type LegacyLogInInfo, type LogInExtraInfo, - type LogInResult, - type LogInStartingPayload, + 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 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, +dispatchActionPromise: DispatchActionPromise, - +legacyLogIn: (logInInfo: LogInInfo) => Promise, + +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 initialNotificationsEncryptedMessage = await this.props.getInitialNotificationsEncryptedMessage(); void this.props.dispatchActionPromise( legacyLogInActionTypes, this.legacyLogInAction({ ...extraInfo, initialNotificationsEncryptedMessage, }), undefined, - ({ calendarQuery: extraInfo.calendarQuery }: LogInStartingPayload), + ({ calendarQuery: extraInfo.calendarQuery }: LegacyLogInStartingPayload), ); }; - async legacyLogInAction(extraInfo: LogInExtraInfo): Promise { + async legacyLogInAction( + extraInfo: LogInExtraInfo, + ): 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 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 cfc271967..a7ec508f6 100644 --- a/native/account/registration/registration-server-call.js +++ b/native/account/registration/registration-server-call.js @@ -1,423 +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 LogInStartingPayload, + 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 { 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 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 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 }: LogInStartingPayload), + ({ + calendarQuery: extraInfo.calendarQuery, + }: LegacyLogInStartingPayload), ); await legacyKeyserverRegisterPromise; }, [logInExtraInfo, 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 70abfe2f6..7d06cf503 100644 --- a/native/account/siwe-hooks.js +++ b/native/account/siwe-hooks.js @@ -1,122 +1,124 @@ // @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 { - LogInStartingPayload, + LegacyLogInStartingPayload, LogInExtraInfo, } 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'; 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 }>, callSingleKeyserverEndpointOptions: ?CallSingleKeyserverEndpointOptions, ) => legacySiweAuthCall( { message, signature, ...extraInfo, }, callSingleKeyserverEndpointOptions, ), [legacySiweAuthCall], ); const logInExtraInfo = useSelector(nativeLogInExtraInfoSelector); const getInitialNotificationsEncryptedMessage = useInitialNotificationsEncryptedMessage(authoritativeKeyserverID); const dispatchActionPromise = useDispatchActionPromise(); return React.useCallback( async ( { message, signature, doNotRegister }, callSingleKeyserverEndpointOptions, ) => { const extraInfo = await logInExtraInfo(); const initialNotificationsEncryptedMessage = await getInitialNotificationsEncryptedMessage({ callSingleKeyserverEndpointOptions, }); const siwePromise = callSIWE( message, signature, { ...extraInfo, initialNotificationsEncryptedMessage, doNotRegister, }, callSingleKeyserverEndpointOptions, ); void dispatchActionPromise( legacySiweAuthActionTypes, siwePromise, undefined, - ({ calendarQuery: extraInfo.calendarQuery }: LogInStartingPayload), + ({ + calendarQuery: extraInfo.calendarQuery, + }: LegacyLogInStartingPayload), ); await siwePromise; }, [ logInExtraInfo, 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/web/account/siwe-login-form.react.js b/web/account/siwe-login-form.react.js index 2b51e9862..a72404731 100644 --- a/web/account/siwe-login-form.react.js +++ b/web/account/siwe-login-form.react.js @@ -1,313 +1,315 @@ // @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 { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import type { - LogInStartingPayload, + LegacyLogInStartingPayload, LogInExtraInfo, } 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 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) => { 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), undefined, - ({ calendarQuery: logInExtraInfo.calendarQuery }: LogInStartingPayload), + ({ + calendarQuery: logInExtraInfo.calendarQuery, + }: LegacyLogInStartingPayload), ); }, [callLegacySIWEAuthEndpoint, dispatchActionPromise, logInExtraInfo], ); 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 209e48268..f343af1a8 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 { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { oldValidUsernameRegex, validEmailRegex, } from 'lib/shared/account-utils.js'; import type { LogInExtraInfo, - LogInStartingPayload, + 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 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) => { 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), undefined, ({ calendarQuery: loginExtraInfo.calendarQuery, - }: LogInStartingPayload), + }: LegacyLogInStartingPayload), ); } }, [ dispatchActionPromise, identityPasswordLogInAction, legacyLogInAction, loginExtraInfo, 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;