diff --git a/native/account/registration/avatar-selection.react.js b/native/account/registration/avatar-selection.react.js index 2e1666245..f1eba5c06 100644 --- a/native/account/registration/avatar-selection.react.js +++ b/native/account/registration/avatar-selection.react.js @@ -1,160 +1,185 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { Text, View } from 'react-native'; import RegistrationButtonContainer from './registration-button-container.react.js'; import RegistrationButton from './registration-button.react.js'; import RegistrationContainer from './registration-container.react.js'; import RegistrationContentContainer from './registration-content-container.react.js'; +import { RegistrationContext } from './registration-context.js'; import type { RegistrationNavigationProp } from './registration-navigator.react.js'; import type { CoolOrNerdMode, AccountSelection, AvatarData, } from './registration-types.js'; import { EditUserAvatarContext, type UserAvatarSelection, } from '../../avatars/edit-user-avatar-provider.react.js'; import EditUserAvatar from '../../avatars/edit-user-avatar.react.js'; import type { NavigationRoute } from '../../navigation/route-names.js'; import { useStyles } from '../../themes/colors.js'; export type AvatarSelectionParams = { +userSelections: { +coolOrNerdMode: CoolOrNerdMode, +keyserverUsername: string, +accountSelection: AccountSelection, }, }; const ensDefaultSelection = { needsUpload: false, updateUserAvatarRequest: { type: 'ens' }, clientAvatar: { type: 'ens' }, }; type Props = { +navigation: RegistrationNavigationProp<'AvatarSelection'>, +route: NavigationRoute<'AvatarSelection'>, }; function AvatarSelection(props: Props): React.Node { const { userSelections } = props.route.params; const { accountSelection } = userSelections; const username = accountSelection.accountType === 'username' ? accountSelection.username : accountSelection.address; const editUserAvatarContext = React.useContext(EditUserAvatarContext); invariant(editUserAvatarContext, 'editUserAvatarContext should be set'); const { setRegistrationMode } = editUserAvatarContext; const prefetchedAvatarURI = accountSelection.accountType === 'ethereum' ? accountSelection.avatarURI : undefined; const [avatarData, setAvatarData] = React.useState( prefetchedAvatarURI ? ensDefaultSelection : undefined, ); const setClientAvatarFromSelection = React.useCallback( (selection: UserAvatarSelection) => { if (selection.needsUpload) { setAvatarData({ ...selection, clientAvatar: { type: 'image', uri: selection.mediaSelection.uri, }, }); } else if (selection.updateUserAvatarRequest.type !== 'remove') { const clientRequest = selection.updateUserAvatarRequest; invariant( clientRequest.type !== 'image', 'image avatars need to be uploaded', ); setAvatarData({ ...selection, clientAvatar: clientRequest, }); } else { setAvatarData(undefined); } }, [], ); + const [registrationInProgress, setRegistrationInProgress] = + React.useState(false); + React.useEffect(() => { + if (registrationInProgress) { + return undefined; + } setRegistrationMode({ registrationMode: 'on', successCallback: setClientAvatarFromSelection, }); return () => { setRegistrationMode({ registrationMode: 'off' }); }; - }, [setRegistrationMode, setClientAvatarFromSelection]); + }, [ + registrationInProgress, + setRegistrationMode, + setClientAvatarFromSelection, + ]); + + const registrationContext = React.useContext(RegistrationContext); + invariant(registrationContext, 'registrationContext should be set'); + const { register } = registrationContext; - const onProceed = React.useCallback(() => {}, []); + const onProceed = React.useCallback(async () => { + setRegistrationInProgress(true); + try { + await register({ + ...userSelections, + avatarData, + }); + } finally { + setRegistrationInProgress(false); + } + }, [register, userSelections, avatarData]); const clientAvatar = avatarData?.clientAvatar; const userInfoOverride = React.useMemo( () => ({ username, avatar: clientAvatar, }), [username, clientAvatar], ); const styles = useStyles(unboundStyles); return ( Pick an avatar ); } const unboundStyles = { scrollViewContentContainer: { paddingHorizontal: 0, }, header: { fontSize: 24, color: 'panelForegroundLabel', paddingBottom: 16, paddingHorizontal: 16, }, stagedAvatarSection: { marginTop: 16, backgroundColor: 'panelForeground', paddingVertical: 24, alignItems: 'center', }, editUserAvatar: { alignItems: 'center', justifyContent: 'center', }, }; export default AvatarSelection; diff --git a/native/account/registration/registration-context-provider.react.js b/native/account/registration/registration-context-provider.react.js index 33dc9ac81..9cd2d0b6e 100644 --- a/native/account/registration/registration-context-provider.react.js +++ b/native/account/registration/registration-context-provider.react.js @@ -1,19 +1,27 @@ // @flow import * as React from 'react'; import { RegistrationContext } from './registration-context.js'; +import { useRegistrationServerCall } from './registration-server-call.js'; type Props = { +children: React.Node, }; function RegistrationContextProvider(props: Props): React.Node { - const contextValue = React.useMemo(() => ({}), []); + const registrationServerCall = useRegistrationServerCall(); + const contextValue = React.useMemo( + () => ({ + register: registrationServerCall, + }), + [registrationServerCall], + ); + return ( {props.children} ); } export { RegistrationContextProvider }; diff --git a/native/account/registration/registration-context.js b/native/account/registration/registration-context.js index c5b2e6fd7..92ac217fa 100644 --- a/native/account/registration/registration-context.js +++ b/native/account/registration/registration-context.js @@ -1,10 +1,14 @@ // @flow import * as React from 'react'; -export type RegistrationContextType = {}; +import type { RegistrationServerCallInput } from './registration-types.js'; + +export type RegistrationContextType = { + +register: RegistrationServerCallInput => Promise, +}; const RegistrationContext: React.Context = React.createContext(); export { RegistrationContext }; diff --git a/native/account/registration/registration-server-call.js b/native/account/registration/registration-server-call.js new file mode 100644 index 000000000..b75828676 --- /dev/null +++ b/native/account/registration/registration-server-call.js @@ -0,0 +1,118 @@ +// @flow + +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, +} from './registration-types.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'; + +function useRegistrationServerCall(): RegistrationServerCallInput => Promise { + 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) => { + const extraInfo = await logInExtraInfo(); + const registerPromise = (async () => { + try { + const result = await callRegister({ + ...extraInfo, + username: accountSelection.username, + password: accountSelection.password, + }); + 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 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, + }, + }); + }, + [registerUsernameAccount, siweServerCall, dispatch], + ); +} + +export { useRegistrationServerCall }; diff --git a/native/account/registration/registration-types.js b/native/account/registration/registration-types.js index d573bc3d0..71c9189f3 100644 --- a/native/account/registration/registration-types.js +++ b/native/account/registration/registration-types.js @@ -1,38 +1,45 @@ // @flow import type { UpdateUserAvatarRequest, ClientAvatar, } from 'lib/types/avatar-types.js'; import type { NativeMediaSelection } from 'lib/types/media-types.js'; import type { SIWEResult } from 'lib/types/siwe-types.js'; export type CoolOrNerdMode = 'cool' | 'nerd'; export type EthereumAccountSelection = { +accountType: 'ethereum', ...SIWEResult, +avatarURI: ?string, }; export type UsernameAccountSelection = { +accountType: 'username', +username: string, +password: string, }; export type AccountSelection = | EthereumAccountSelection | UsernameAccountSelection; export type AvatarData = | { +needsUpload: true, +mediaSelection: NativeMediaSelection, +clientAvatar: ClientAvatar, } | { +needsUpload: false, +updateUserAvatarRequest: UpdateUserAvatarRequest, +clientAvatar: ClientAvatar, }; + +export type RegistrationServerCallInput = { + +coolOrNerdMode: CoolOrNerdMode, + +keyserverUsername: string, + +accountSelection: AccountSelection, + +avatarData: ?AvatarData, +};