diff --git a/lib/actions/siwe-actions.js b/lib/actions/siwe-actions.js index eefe5716b..4fae61b11 100644 --- a/lib/actions/siwe-actions.js +++ b/lib/actions/siwe-actions.js @@ -1,70 +1,79 @@ // @flow import { mergeUserInfos } from './user-actions.js'; import threadWatcher from '../shared/thread-watcher.js'; import { type LogInResult, logInActionSources, } from '../types/account-types.js'; import type { SIWEAuthServerCall } from '../types/siwe-types.js'; -import type { CallServerEndpoint } from '../utils/call-server-endpoint.js'; +import type { + CallServerEndpoint, + CallServerEndpointOptions, +} from '../utils/call-server-endpoint.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 = (callServerEndpoint: CallServerEndpoint): (() => Promise) => async () => { const response = await callServerEndpoint('siwe_nonce'); return response.nonce; }; const siweAuthActionTypes = Object.freeze({ started: 'SIWE_AUTH_STARTED', success: 'SIWE_AUTH_SUCCESS', failed: 'SIWE_AUTH_FAILED', }); const siweAuthCallServerEndpointOptions = { timeout: 60000 }; const siweAuth = ( callServerEndpoint: CallServerEndpoint, - ): ((siweAuthPayload: SIWEAuthServerCall) => Promise) => - async siweAuthPayload => { + ): (( + siweAuthPayload: SIWEAuthServerCall, + options?: ?CallServerEndpointOptions, + ) => Promise) => + async (siweAuthPayload, options) => { const watchedIDs = threadWatcher.getWatchedIDs(); const response = await callServerEndpoint( 'siwe_auth', { ...siweAuthPayload, watchedIDs, platformDetails: getConfig().platformDetails, }, - siweAuthCallServerEndpointOptions, + { + ...siweAuthCallServerEndpointOptions, + ...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: response.serverTime, }, userInfos, updatesCurrentAsOf: response.serverTime, logInActionSource: logInActionSources.logInFromNativeSIWE, notAcknowledgedPolicies: response.notAcknowledgedPolicies, }; }; export { getSIWENonceActionTypes, getSIWENonce, siweAuthActionTypes, siweAuth }; diff --git a/lib/actions/user-actions.js b/lib/actions/user-actions.js index deccc5a46..158960c6e 100644 --- a/lib/actions/user-actions.js +++ b/lib/actions/user-actions.js @@ -1,335 +1,350 @@ // @flow import threadWatcher from '../shared/thread-watcher.js'; import type { LogOutResult, LogInInfo, LogInResult, RegisterResult, RegisterInfo, UpdateUserSettingsRequest, PolicyAcknowledgmentRequest, } from '../types/account-types.js'; import type { UpdateUserAvatarRequest, UpdateUserAvatarResponse, } from '../types/avatar-types.js'; import type { GetSessionPublicKeysArgs, GetOlmSessionInitializationDataResponse, } from '../types/request-types.js'; import type { UserSearchResult, ExactUserSearchResult, } from '../types/search-types.js'; import type { SessionPublicKeys, PreRequestUserState, } from '../types/session-types.js'; import type { SubscriptionUpdateRequest, SubscriptionUpdateResult, } from '../types/subscription-types.js'; import type { UserInfo, PasswordUpdate } from '../types/user-types.js'; -import type { CallServerEndpoint } from '../utils/call-server-endpoint.js'; +import type { + CallServerEndpoint, + CallServerEndpointOptions, +} from '../utils/call-server-endpoint.js'; import { getConfig } from '../utils/config.js'; import sleep from '../utils/sleep.js'; const logOutActionTypes = Object.freeze({ started: 'LOG_OUT_STARTED', success: 'LOG_OUT_SUCCESS', failed: 'LOG_OUT_FAILED', }); const logOut = ( callServerEndpoint: CallServerEndpoint, ): ((preRequestUserState: PreRequestUserState) => Promise) => async preRequestUserState => { let response = null; try { response = await Promise.race([ callServerEndpoint('log_out', {}), (async () => { await sleep(500); throw new Error('log_out took more than 500ms'); })(), ]); } catch {} const currentUserInfo = response ? response.currentUserInfo : null; return { currentUserInfo, preRequestUserState }; }; const deleteAccountActionTypes = Object.freeze({ started: 'DELETE_ACCOUNT_STARTED', success: 'DELETE_ACCOUNT_SUCCESS', failed: 'DELETE_ACCOUNT_FAILED', }); const deleteAccount = ( callServerEndpoint: CallServerEndpoint, ): (( password: ?string, preRequestUserState: PreRequestUserState, ) => Promise) => async (password, preRequestUserState) => { const response = await callServerEndpoint('delete_account', { password }); return { currentUserInfo: response.currentUserInfo, preRequestUserState }; }; const registerActionTypes = Object.freeze({ started: 'REGISTER_STARTED', success: 'REGISTER_SUCCESS', failed: 'REGISTER_FAILED', }); const registerCallServerEndpointOptions = { timeout: 60000 }; const register = ( callServerEndpoint: CallServerEndpoint, - ): ((registerInfo: RegisterInfo) => Promise) => - async registerInfo => { + ): (( + registerInfo: RegisterInfo, + options?: CallServerEndpointOptions, + ) => Promise) => + async (registerInfo, options) => { const response = await callServerEndpoint( 'create_account', { ...registerInfo, platformDetails: getConfig().platformDetails, }, - registerCallServerEndpointOptions, + { + ...registerCallServerEndpointOptions, + ...options, + }, ); return { currentUserInfo: response.currentUserInfo, rawMessageInfos: response.rawMessageInfos, threadInfos: response.cookieChange.threadInfos, userInfos: response.cookieChange.userInfos, calendarQuery: registerInfo.calendarQuery, }; }; function mergeUserInfos(...userInfoArrays: UserInfo[][]): UserInfo[] { const merged = {}; 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; } const logInActionTypes = Object.freeze({ started: 'LOG_IN_STARTED', success: 'LOG_IN_SUCCESS', failed: 'LOG_IN_FAILED', }); const logInCallServerEndpointOptions = { timeout: 60000 }; const logIn = ( callServerEndpoint: CallServerEndpoint, ): ((logInInfo: LogInInfo) => Promise) => async logInInfo => { const watchedIDs = threadWatcher.getWatchedIDs(); const { logInActionSource, ...restLogInInfo } = logInInfo; const response = await callServerEndpoint( 'log_in', { ...restLogInInfo, source: logInActionSource, watchedIDs, platformDetails: getConfig().platformDetails, }, logInCallServerEndpointOptions, ); const userInfos = mergeUserInfos( response.userInfos, response.cookieChange.userInfos, ); return { threadInfos: response.cookieChange.threadInfos, currentUserInfo: response.currentUserInfo, calendarResult: { calendarQuery: logInInfo.calendarQuery, rawEntryInfos: response.rawEntryInfos, }, messagesResult: { messageInfos: response.rawMessageInfos, truncationStatus: response.truncationStatuses, watchedIDsAtRequestTime: watchedIDs, currentAsOf: response.serverTime, }, userInfos, updatesCurrentAsOf: response.serverTime, logInActionSource: logInInfo.logInActionSource, notAcknowledgedPolicies: response.notAcknowledgedPolicies, }; }; const changeUserPasswordActionTypes = Object.freeze({ started: 'CHANGE_USER_PASSWORD_STARTED', success: 'CHANGE_USER_PASSWORD_SUCCESS', failed: 'CHANGE_USER_PASSWORD_FAILED', }); const changeUserPassword = ( callServerEndpoint: CallServerEndpoint, ): ((passwordUpdate: PasswordUpdate) => Promise) => async passwordUpdate => { await callServerEndpoint('update_account', passwordUpdate); }; const searchUsersActionTypes = Object.freeze({ started: 'SEARCH_USERS_STARTED', success: 'SEARCH_USERS_SUCCESS', failed: 'SEARCH_USERS_FAILED', }); const searchUsers = ( callServerEndpoint: CallServerEndpoint, ): ((usernamePrefix: string) => Promise) => async usernamePrefix => { const response = await callServerEndpoint('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 = ( callServerEndpoint: CallServerEndpoint, ): ((username: string) => Promise) => async username => { const response = await callServerEndpoint('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 = ( callServerEndpoint: CallServerEndpoint, ): (( subscriptionUpdate: SubscriptionUpdateRequest, ) => Promise) => async subscriptionUpdate => { const response = await callServerEndpoint( 'update_user_subscription', subscriptionUpdate, ); return { threadID: subscriptionUpdate.threadID, subscription: response.threadSubscription, }; }; const setUserSettingsActionTypes = Object.freeze({ started: 'SET_USER_SETTINGS_STARTED', success: 'SET_USER_SETTINGS_SUCCESS', failed: 'SET_USER_SETTINGS_FAILED', }); const setUserSettings = ( callServerEndpoint: CallServerEndpoint, ): ((userSettingsRequest: UpdateUserSettingsRequest) => Promise) => async userSettingsRequest => { await callServerEndpoint('update_user_settings', userSettingsRequest); }; const getSessionPublicKeys = ( callServerEndpoint: CallServerEndpoint, ): ((data: GetSessionPublicKeysArgs) => Promise) => async data => { return await callServerEndpoint('get_session_public_keys', data); }; 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 = ( callServerEndpoint: CallServerEndpoint, - ): (() => Promise) => - async () => { - return await callServerEndpoint('get_olm_session_initialization_data', {}); + ): (( + options?: ?CallServerEndpointOptions, + ) => Promise) => + async options => { + return await callServerEndpoint( + 'get_olm_session_initialization_data', + {}, + options, + ); }; const policyAcknowledgmentActionTypes = Object.freeze({ started: 'POLICY_ACKNOWLEDGMENT_STARTED', success: 'POLICY_ACKNOWLEDGMENT_SUCCESS', failed: 'POLICY_ACKNOWLEDGMENT_FAILED', }); const policyAcknowledgment = ( callServerEndpoint: CallServerEndpoint, ): ((policyRequest: PolicyAcknowledgmentRequest) => Promise) => async policyRequest => { await callServerEndpoint('policy_acknowledgment', policyRequest); }; const updateUserAvatarActionTypes = Object.freeze({ started: 'UPDATE_USER_AVATAR_STARTED', success: 'UPDATE_USER_AVATAR_SUCCESS', failed: 'UPDATE_USER_AVATAR_FAILED', }); const updateUserAvatar = ( callServerEndpoint: CallServerEndpoint, ): (( avatarDBContent: UpdateUserAvatarRequest, ) => Promise) => async avatarDBContent => { const { updates }: UpdateUserAvatarResponse = await callServerEndpoint( 'update_user_avatar', avatarDBContent, ); return { updates }; }; export { changeUserPasswordActionTypes, changeUserPassword, deleteAccount, deleteAccountActionTypes, getSessionPublicKeys, getOlmSessionInitializationDataActionTypes, getOlmSessionInitializationData, mergeUserInfos, logIn, logInActionTypes, logOut, logOutActionTypes, register, registerActionTypes, searchUsers, searchUsersActionTypes, exactSearchUser, exactSearchUserActionTypes, setUserSettings, setUserSettingsActionTypes, updateSubscription, updateSubscriptionActionTypes, policyAcknowledgment, policyAcknowledgmentActionTypes, updateUserAvatarActionTypes, updateUserAvatar, }; diff --git a/lib/utils/call-server-endpoint.js b/lib/utils/call-server-endpoint.js index 425726b22..b001f0fdf 100644 --- a/lib/utils/call-server-endpoint.js +++ b/lib/utils/call-server-endpoint.js @@ -1,219 +1,222 @@ // @flow import { getConfig } from './config.js'; import { ServerError, FetchTimeout, SocketOffline, SocketTimeout, } from './errors.js'; import sleep from './sleep.js'; import { uploadBlob, type UploadBlob } from './upload-blob.js'; import { callServerEndpointTimeout } from '../shared/timeouts.js'; import type { Shape } from '../types/core.js'; import { type Endpoint, type SocketAPIHandler, endpointIsSocketPreferred, endpointIsSocketOnly, } from '../types/endpoints.js'; import { forcePolicyAcknowledgmentActionType } from '../types/policy-types.js'; import type { Dispatch } from '../types/redux-types.js'; import type { ServerSessionChange, ClientSessionChange, } from '../types/session-types.js'; import type { ConnectionStatus } from '../types/socket-types'; import type { CurrentUserInfo } from '../types/user-types.js'; export type CallServerEndpointOptions = Shape<{ // null timeout means no timeout, which is the default for uploadBlob +timeout: ?number, // in milliseconds // getResultInfo will be called right before callServerEndpoint successfully // resolves and includes additional information about the request +getResultInfo: (resultInfo: CallServerEndpointResultInfo) => mixed, +blobUpload: boolean | UploadBlob, // the rest (onProgress, abortHandler) only work with blobUpload +onProgress: (percent: number) => void, // abortHandler will receive an abort function once the upload starts +abortHandler: (abort: () => void) => void, + // Overrides urlPrefix in Redux + +urlPrefixOverride: string, }>; export type CallServerEndpointResultInfoInterface = 'socket' | 'REST'; export type CallServerEndpointResultInfo = { +interface: CallServerEndpointResultInfoInterface, }; export type CallServerEndpointResponse = Shape<{ +cookieChange: ServerSessionChange, +currentUserInfo: CurrentUserInfo, +error: string, +payload: Object, }>; // You'll notice that this is not the type of the callServerEndpoint // function below. This is because the first several parameters to that // function get bound in by the helpers in lib/utils/action-utils.js. // This type represents the form of the callServerEndpoint function that // gets passed to the action function in lib/actions. export type CallServerEndpoint = ( endpoint: Endpoint, input: Object, options?: ?CallServerEndpointOptions, ) => Promise; type RequestData = { input: { [key: string]: mixed }, cookie?: ?string, sessionID?: ?string, }; // If cookie is undefined, then we will defer to the underlying environment to // handle cookies, and we won't worry about them. We do this on the web since // our cookies are httponly to protect against XSS attacks. On the other hand, // on native we want to keep track of the cookies since we don't trust the // underlying implementations and prefer for things to be explicit, and XSS // isn't a thing on native. Note that for native, cookie might be null // (indicating we don't have one), and we will then set an empty Cookie header. async function callServerEndpoint( cookie: ?string, setNewSession: (sessionChange: ClientSessionChange, error: ?string) => void, waitIfCookieInvalidated: () => Promise, cookieInvalidationRecovery: ( sessionChange: ClientSessionChange, ) => Promise, urlPrefix: string, sessionID: ?string, connectionStatus: ConnectionStatus, socketAPIHandler: ?SocketAPIHandler, endpoint: Endpoint, input: { [key: string]: mixed }, dispatch: Dispatch, options?: ?CallServerEndpointOptions, ): Promise { const possibleReplacement = await waitIfCookieInvalidated(); if (possibleReplacement) { return await possibleReplacement(endpoint, input, options); } if ( endpointIsSocketPreferred(endpoint) && connectionStatus === 'connected' && socketAPIHandler ) { try { const result = await socketAPIHandler({ endpoint, input }); options?.getResultInfo?.({ interface: 'socket' }); return result; } catch (e) { if (endpointIsSocketOnly(endpoint)) { throw e; } else if (e instanceof SocketOffline) { // nothing } else if (e instanceof SocketTimeout) { // nothing } else { throw e; } } } if (endpointIsSocketOnly(endpoint)) { throw new SocketOffline('socket_offline'); } - const url = urlPrefix ? `${urlPrefix}/${endpoint}` : endpoint; + const resolvedURLPrefix = options?.urlPrefixOverride ?? urlPrefix; + const url = resolvedURLPrefix ? `${resolvedURLPrefix}/${endpoint}` : endpoint; let json; if (options && options.blobUpload) { const uploadBlobCallback = typeof options.blobUpload === 'function' ? options.blobUpload : uploadBlob; json = await uploadBlobCallback(url, cookie, sessionID, input, options); } else { const mergedData: RequestData = { input }; if (getConfig().setCookieOnRequest) { // We make sure that if setCookieOnRequest is true, we never set cookie to // undefined. null has a special meaning here: we don't currently have a // cookie, and we want the server to specify the new cookie it will // generate in the response body rather than the response header. See // session-types.js for more details on why we specify cookies in the body mergedData.cookie = cookie ? cookie : null; } if (getConfig().setSessionIDOnRequest) { // We make sure that if setSessionIDOnRequest is true, we never set // sessionID to undefined. null has a special meaning here: we cannot // consider the cookieID to be a unique session identifier, but we do not // have a sessionID to use either. This should only happen when the user // is not logged in on web. mergedData.sessionID = sessionID ? sessionID : null; } const callEndpointPromise = (async (): Promise => { const response = await fetch(url, { method: 'POST', // This is necessary to allow cookie headers to get passed down to us credentials: 'same-origin', body: JSON.stringify(mergedData), headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, }); const text = await response.text(); try { return JSON.parse(text); } catch (e) { console.log(text); throw e; } })(); const timeout = options && options.timeout ? options.timeout : callServerEndpointTimeout; if (!timeout) { json = await callEndpointPromise; } else { const rejectPromise = (async () => { await sleep(timeout); throw new FetchTimeout( `callServerEndpoint timed out call to ${endpoint}`, endpoint, ); })(); json = await Promise.race([callEndpointPromise, rejectPromise]); } } const { cookieChange, error, payload, currentUserInfo } = json; const sessionChange: ?ServerSessionChange = cookieChange; if (sessionChange) { const { threadInfos, userInfos, ...rest } = sessionChange; const clientSessionChange = rest.cookieInvalidated ? rest : { cookieInvalidated: false, currentUserInfo, ...rest }; if (clientSessionChange.cookieInvalidated) { const maybeReplacement = await cookieInvalidationRecovery( clientSessionChange, ); if (maybeReplacement) { return await maybeReplacement(endpoint, input, options); } } setNewSession(clientSessionChange, error); } if (error === 'policies_not_accepted') { dispatch({ type: forcePolicyAcknowledgmentActionType, payload, }); } if (error) { throw new ServerError(error, payload); } options?.getResultInfo?.({ interface: 'REST' }); return json; } export default callServerEndpoint; diff --git a/native/account/registration/registration-server-call.js b/native/account/registration/registration-server-call.js index 9ec306a91..469e062a7 100644 --- a/native/account/registration/registration-server-call.js +++ b/native/account/registration/registration-server-call.js @@ -1,221 +1,231 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { Alert, Platform } from 'react-native'; import { useDispatch } from 'react-redux'; import { setDataLoadedActionType } from 'lib/actions/client-db-store-actions.js'; import { registerActionTypes, register } from 'lib/actions/user-actions.js'; import type { LogInStartingPayload } from 'lib/types/account-types.js'; import { useServerCall, useDispatchActionPromise, } from 'lib/utils/action-utils.js'; import type { RegistrationServerCallInput, UsernameAccountSelection, AvatarData, } from './registration-types.js'; import { useUploadSelectedMedia } from '../../avatars/avatar-hooks.js'; import { EditUserAvatarContext } from '../../avatars/edit-user-avatar-provider.react.js'; import { NavContext } from '../../navigation/navigation-context.js'; import { useSelector } from '../../redux/redux-utils.js'; import { nativeLogInExtraInfoSelector } from '../../selectors/account-selectors.js'; import { setNativeCredentials } from '../native-credentials.js'; import { useSIWEServerCall } 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: 'waiting_for_registration_call', +avatarData: ?AvatarData, +resolve: () => void, +reject: Error => void, }; const inactiveStep = { step: 'inactive' }; function useRegistrationServerCall(): RegistrationServerCallInput => Promise { const [currentStep, setCurrentStep] = React.useState(inactiveStep); // STEP 1: ACCOUNT REGISTRATION const navContext = React.useContext(NavContext); const logInExtraInfo = useSelector(state => nativeLogInExtraInfoSelector({ redux: state, navContext, }), ); const dispatchActionPromise = useDispatchActionPromise(); const callRegister = useServerCall(register); const registerUsernameAccount = React.useCallback( - async (accountSelection: UsernameAccountSelection) => { + async ( + accountSelection: UsernameAccountSelection, + keyserverURL: string, + ) => { const extraInfo = await logInExtraInfo(); const registerPromise = (async () => { try { - const result = await callRegister({ - ...extraInfo, - username: accountSelection.username, - password: accountSelection.password, - }); + const result = await callRegister( + { + ...extraInfo, + username: accountSelection.username, + password: accountSelection.password, + }, + { + urlPrefixOverride: keyserverURL, + }, + ); await setNativeCredentials({ username: result.currentUserInfo.username, password: accountSelection.password, }); return result; } catch (e) { if (e.message === 'username_reserved') { Alert.alert( 'Username reserved', 'This username is currently reserved. Please contact support@' + 'comm.app if you would like to claim this account.', ); } else if (e.message === 'username_taken') { Alert.alert( 'Username taken', 'An account with that username already exists', ); } else if (e.message === 'client_version_unsupported') { const app = Platform.select({ ios: 'App Store', android: 'Play Store', }); Alert.alert( 'App out of date', 'Your app version is pretty old, and the server doesn’t know how ' + `to speak to it anymore. Please use the ${app} app to update!`, ); } else { Alert.alert('Unknown error', 'Uhh... try again?'); } throw e; } })(); dispatchActionPromise( registerActionTypes, registerPromise, undefined, ({ calendarQuery: extraInfo.calendarQuery }: LogInStartingPayload), ); await registerPromise; }, [logInExtraInfo, callRegister, dispatchActionPromise], ); const siweServerCallParams = React.useMemo(() => { const onServerCallFailure = () => { Alert.alert('Unknown error', 'Uhh... try again?'); }; return { onFailure: onServerCallFailure }; }, []); const siweServerCall = useSIWEServerCall(siweServerCallParams); 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 } = input; + const { accountSelection, avatarData, keyserverURL } = input; if (accountSelection.accountType === 'username') { - await registerUsernameAccount(accountSelection); + await registerUsernameAccount(accountSelection, keyserverURL); } else { - await siweServerCall(accountSelection); + await siweServerCall(accountSelection, { + urlPrefixOverride: keyserverURL, + }); } setCurrentStep({ step: 'waiting_for_registration_call', avatarData, resolve, reject, }); } catch (e) { reject(e); } }, ), [currentStep, registerUsernameAccount, siweServerCall], ); // STEP 2: SETTING AVATAR const uploadSelectedMedia = useUploadSelectedMedia(); const editUserAvatarContext = React.useContext(EditUserAvatarContext); invariant(editUserAvatarContext, 'editUserAvatarContext should be set'); const { setUserAvatar } = editUserAvatarContext; const hasCurrentUserInfo = useSelector( state => !!state.currentUserInfo && !state.currentUserInfo.anonymous, ); const dispatch = useDispatch(); const avatarBeingSetRef = React.useRef(false); React.useEffect(() => { if ( !hasCurrentUserInfo || currentStep.step !== 'waiting_for_registration_call' || avatarBeingSetRef.current ) { return; } avatarBeingSetRef.current = true; const { avatarData, resolve } = currentStep; (async () => { try { if (!avatarData) { return; } let updateUserAvatarRequest; if (!avatarData.needsUpload) { ({ updateUserAvatarRequest } = avatarData); } else { const { mediaSelection } = avatarData; updateUserAvatarRequest = await uploadSelectedMedia(mediaSelection); if (!updateUserAvatarRequest) { return; } } await setUserAvatar(updateUserAvatarRequest); } finally { dispatch({ type: setDataLoadedActionType, payload: { dataLoaded: true, }, }); setCurrentStep(inactiveStep); avatarBeingSetRef.current = false; resolve(); } })(); }, [ currentStep, hasCurrentUserInfo, uploadSelectedMedia, setUserAvatar, dispatch, ]); return returnedFunc; } export { useRegistrationServerCall }; diff --git a/native/account/siwe-hooks.js b/native/account/siwe-hooks.js index 585c080ba..d3d1063bc 100644 --- a/native/account/siwe-hooks.js +++ b/native/account/siwe-hooks.js @@ -1,89 +1,98 @@ // @flow import * as React from 'react'; import { siweAuth, siweAuthActionTypes } from 'lib/actions/siwe-actions.js'; import type { LogInStartingPayload } from 'lib/types/account-types.js'; import { useServerCall, useDispatchActionPromise, } from 'lib/utils/action-utils.js'; +import type { CallServerEndpointOptions } from 'lib/utils/call-server-endpoint.js'; import { NavContext } from '../navigation/navigation-context.js'; import { useSelector } from '../redux/redux-utils.js'; import { nativeLogInExtraInfoSelector } from '../selectors/account-selectors.js'; import { useInitialNotificationsEncryptedMessage } from '../utils/crypto-utils.js'; type SIWEServerCallParams = { +message: string, +signature: string, ... }; type UseSIWEServerCallParams = { +onFailure: () => mixed, }; function useSIWEServerCall( params: UseSIWEServerCallParams, -): SIWEServerCallParams => Promise { +): (SIWEServerCallParams, ?CallServerEndpointOptions) => Promise { const { onFailure } = params; const siweAuthCall = useServerCall(siweAuth); const callSIWE = React.useCallback( - async (message, signature, extraInfo) => { + async (message, signature, extraInfo, options) => { try { - return await siweAuthCall({ - message, - signature, - ...extraInfo, - }); + return await siweAuthCall( + { + message, + signature, + ...extraInfo, + }, + options, + ); } catch (e) { onFailure(); throw e; } }, [onFailure, siweAuthCall], ); const navContext = React.useContext(NavContext); const logInExtraInfo = useSelector(state => nativeLogInExtraInfoSelector({ redux: state, navContext, }), ); const getInitialNotificationsEncryptedMessage = useInitialNotificationsEncryptedMessage(); const dispatchActionPromise = useDispatchActionPromise(); return React.useCallback( - async ({ message, signature }) => { + async ({ message, signature }, options) => { const extraInfo = await logInExtraInfo(); const initialNotificationsEncryptedMessage = - await getInitialNotificationsEncryptedMessage(); + await getInitialNotificationsEncryptedMessage(options); - const siwePromise = callSIWE(message, signature, { - ...extraInfo, - initialNotificationsEncryptedMessage, - }); + const siwePromise = callSIWE( + message, + signature, + { + ...extraInfo, + initialNotificationsEncryptedMessage, + }, + options, + ); dispatchActionPromise( siweAuthActionTypes, siwePromise, undefined, ({ calendarQuery: extraInfo.calendarQuery }: LogInStartingPayload), ); await siwePromise; }, [ logInExtraInfo, dispatchActionPromise, callSIWE, getInitialNotificationsEncryptedMessage, ], ); } export { useSIWEServerCall }; diff --git a/native/utils/crypto-utils.js b/native/utils/crypto-utils.js index 2158a87ee..54c8d6b8e 100644 --- a/native/utils/crypto-utils.js +++ b/native/utils/crypto-utils.js @@ -1,47 +1,55 @@ // @flow import * as React from 'react'; import { getOlmSessionInitializationData, getOlmSessionInitializationDataActionTypes, } from 'lib/actions/user-actions.js'; import { useServerCall, useDispatchActionPromise, } from 'lib/utils/action-utils.js'; +import type { CallServerEndpointOptions } from 'lib/utils/call-server-endpoint.js'; import { commCoreModule } from '../native-modules.js'; -function useInitialNotificationsEncryptedMessage(): () => Promise { +function useInitialNotificationsEncryptedMessage(): ( + callServerEndpointOptions?: ?CallServerEndpointOptions, +) => Promise { const callGetOlmSessionInitializationData = useServerCall( getOlmSessionInitializationData, ); const dispatchActionPromise = useDispatchActionPromise(); - return React.useCallback(async () => { - const olmSessionDataPromise = callGetOlmSessionInitializationData(); - - dispatchActionPromise( - getOlmSessionInitializationDataActionTypes, - olmSessionDataPromise, - ); - - const { signedIdentityKeysBlob, notifInitializationInfo } = - await olmSessionDataPromise; - - const { notificationIdentityPublicKeys } = JSON.parse( - signedIdentityKeysBlob.payload, - ); - - const { prekey, prekeySignature, oneTimeKey } = notifInitializationInfo; - return await commCoreModule.initializeNotificationsSession( - JSON.stringify(notificationIdentityPublicKeys), - prekey, - prekeySignature, - oneTimeKey, - ); - }, [callGetOlmSessionInitializationData, dispatchActionPromise]); + return React.useCallback( + async callServerEndpointOptions => { + const olmSessionDataPromise = callGetOlmSessionInitializationData( + callServerEndpointOptions, + ); + + dispatchActionPromise( + getOlmSessionInitializationDataActionTypes, + olmSessionDataPromise, + ); + + const { signedIdentityKeysBlob, notifInitializationInfo } = + await olmSessionDataPromise; + + const { notificationIdentityPublicKeys } = JSON.parse( + signedIdentityKeysBlob.payload, + ); + + const { prekey, prekeySignature, oneTimeKey } = notifInitializationInfo; + return await commCoreModule.initializeNotificationsSession( + JSON.stringify(notificationIdentityPublicKeys), + prekey, + prekeySignature, + oneTimeKey, + ); + }, + [callGetOlmSessionInitializationData, dispatchActionPromise], + ); } export { useInitialNotificationsEncryptedMessage };