diff --git a/native/account/registration/registration-server-call.js b/native/account/registration/registration-server-call.js --- a/native/account/registration/registration-server-call.js +++ b/native/account/registration/registration-server-call.js @@ -1,5 +1,6 @@ // @flow +import invariant from 'invariant'; import * as React from 'react'; import { Alert, Platform } from 'react-native'; import { useDispatch } from 'react-redux'; @@ -15,14 +16,43 @@ 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({ @@ -96,23 +126,96 @@ }, []); const siweServerCall = useSIWEServerCall(siweServerCallParams); - const dispatch = useDispatch(); - return React.useCallback( - async (input: RegistrationServerCallInput) => { - if (input.accountSelection.accountType === 'username') { - await registerUsernameAccount(input.accountSelection); - } else { - await siweServerCall(input.accountSelection); - } - dispatch({ - type: setDataLoadedActionType, - payload: { - dataLoaded: true, + 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; + if (accountSelection.accountType === 'username') { + await registerUsernameAccount(accountSelection); + } else { + await siweServerCall(accountSelection); + } + setCurrentStep({ + step: 'waiting_for_registration_call', + avatarData, + resolve, + reject, + }); + } catch (e) { + reject(e); + } }, - }); - }, - [registerUsernameAccount, siweServerCall, dispatch], + ), + [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 };