diff --git a/native/account/registration/registration-navigator.react.js b/native/account/registration/registration-navigator.react.js index 3903fcc6f..d2883ed98 100644 --- a/native/account/registration/registration-navigator.react.js +++ b/native/account/registration/registration-navigator.react.js @@ -1,134 +1,205 @@ // @flow import type { StackNavigationProp, StackNavigationHelpers, + ParamListBase, + StackNavigatorProps, + StackNavigationState, + StackOptions, + StackRouterOptions, + StackNavigationEventMap, + ExtraStackNavigatorProps, } from '@react-navigation/core'; -import { createStackNavigator } from '@react-navigation/stack'; +import { + createNavigatorFactory, + useNavigationBuilder, +} from '@react-navigation/native'; +import { StackView } from '@react-navigation/stack'; import * as React from 'react'; import AccountDoesNotExist from './account-does-not-exist.react.js'; import AvatarSelection from './avatar-selection.react.js'; import ConnectEthereum from './connect-ethereum.react.js'; import ConnectFarcaster from './connect-farcaster.react.js'; import CoolOrNerdModeSelection from './cool-or-nerd-mode-selection.react.js'; import EmojiAvatarSelection from './emoji-avatar-selection.react.js'; import ExistingEthereumAccount from './existing-ethereum-account.react.js'; import KeyserverSelection from './keyserver-selection.react.js'; import PasswordSelection from './password-selection.react.js'; +import RegistrationRouter, { + type RegistrationRouterExtraNavigationHelpers, + type RegistrationRouterNavigationAction, +} from './registration-router.js'; import RegistrationTerms from './registration-terms.react.js'; import { CreateSIWEBackupMessage } from './siwe-backup-message-creation.react.js'; import UsernameSelection from './username-selection.react.js'; import RegistrationUserAvatarCameraModal from '../../media/registration-user-avatar-camera-modal.react.js'; import type { RootNavigationProp } from '../../navigation/root-navigator.react.js'; import { KeyserverSelectionRouteName, CoolOrNerdModeSelectionRouteName, ConnectEthereumRouteName, CreateSIWEBackupMessageRouteName, ExistingEthereumAccountRouteName, UsernameSelectionRouteName, ConnectFarcasterRouteName, PasswordSelectionRouteName, AvatarSelectionRouteName, EmojiAvatarSelectionRouteName, RegistrationUserAvatarCameraModalRouteName, RegistrationTermsRouteName, AccountDoesNotExistRouteName, type ScreenParamList, type RegistrationParamList, } from '../../navigation/route-names.js'; +export type RegistrationNavigationHelpers< + ParamList: ParamListBase = ParamListBase, +> = { + ...$Exact>, + ...RegistrationRouterExtraNavigationHelpers, +}; + +type RegistrationNavigatorProps = StackNavigatorProps< + RegistrationNavigationHelpers<>, +>; +function RegistrationNavigator({ + initialRouteName, + children, + screenOptions, + defaultScreenOptions, + screenListeners, + id, + ...rest +}: RegistrationNavigatorProps) { + const { state, descriptors, navigation } = useNavigationBuilder< + StackNavigationState, + RegistrationRouterNavigationAction, + StackOptions, + StackRouterOptions, + RegistrationNavigationHelpers<>, + StackNavigationEventMap, + ExtraStackNavigatorProps, + >(RegistrationRouter, { + id, + initialRouteName, + children, + screenOptions, + defaultScreenOptions, + screenListeners, + }); + return ( + + ); +} +const createRegistrationNavigator = createNavigatorFactory< + StackNavigationState, + StackOptions, + StackNavigationEventMap, + RegistrationNavigationHelpers<>, + ExtraStackNavigatorProps, +>(RegistrationNavigator); + export type RegistrationNavigationProp< RouteName: $Keys = $Keys, -> = StackNavigationProp; +> = { + ...StackNavigationProp, + ...RegistrationRouterExtraNavigationHelpers, +}; -const Registration = createStackNavigator< +const Registration = createRegistrationNavigator< ScreenParamList, RegistrationParamList, StackNavigationHelpers, >(); const screenOptions = { + headerShown: true, headerTransparent: true, headerBackTitleVisible: false, headerTitle: '', headerTintColor: 'white', headerLeftContainerStyle: { paddingLeft: 12, }, }; const cameraScreenOptions = { headerShown: false, }; type Props = { +navigation: RootNavigationProp<'Registration'>, ... }; // eslint-disable-next-line no-unused-vars -function RegistrationNavigator(props: Props): React.Node { +function RegistrationComponent(props: Props): React.Node { return ( ); } -export default RegistrationNavigator; +export default RegistrationComponent; diff --git a/native/account/registration/registration-router.js b/native/account/registration/registration-router.js new file mode 100644 index 000000000..43ee50013 --- /dev/null +++ b/native/account/registration/registration-router.js @@ -0,0 +1,104 @@ +// @flow + +import type { + StackAction, + Route, + Router, + StackRouterOptions, + StackNavigationState, + RouterConfigOptions, +} from '@react-navigation/core'; +import { StackRouter } from '@react-navigation/native'; + +import type { ConnectEthereumParams } from './connect-ethereum.react.js'; +import { reconnectEthereumActionType } from '../../navigation/action-types.js'; +import { removeScreensFromStack } from '../../navigation/navigation-utils.js'; +import { + ConnectEthereumRouteName, + ConnectFarcasterRouteName, +} from '../../navigation/route-names.js'; + +type ReconnectEthereumAction = { + +type: 'RECONNECT_ETHEREUM', + +payload: { + +params: ConnectEthereumParams, + }, +}; +export type RegistrationRouterNavigationAction = + | StackAction + | ReconnectEthereumAction; + +export type RegistrationRouterExtraNavigationHelpers = { + +reconnectEthereum: ConnectEthereumParams => void, +}; + +function RegistrationRouter( + routerOptions: StackRouterOptions, +): Router { + const { + getStateForAction: baseGetStateForAction, + actionCreators: baseActionCreators, + ...rest + } = StackRouter(routerOptions); + return { + ...rest, + getStateForAction: ( + lastState: StackNavigationState, + action: RegistrationRouterNavigationAction, + options: RouterConfigOptions, + ) => { + if (action.type === reconnectEthereumActionType) { + // First, we look to see if ConnectEthereum is already in the stack. + // If it is, we'll pop all later screens and navigate to it. + const routeNames = lastState.routes.map(({ name }) => name); + if (routeNames.includes(ConnectEthereumRouteName)) { + const newState = removeScreensFromStack( + lastState, + (route: Route<>) => + route.name === ConnectEthereumRouteName ? 'break' : 'remove', + ); + const connectEthereumRoute = newState.routes[newState.index]; + const restRoutes = newState.routes.slice(0, -1); + return { + ...newState, + routes: [ + ...restRoutes, + { + ...connectEthereumRoute, + params: action.payload.params, + }, + ], + }; + } + // If it's not in the stack, we'll pop up to ConnectFarcaster + // and then push a new route + const newState = removeScreensFromStack(lastState, (route: Route<>) => + route.name === ConnectFarcasterRouteName ? 'break' : 'remove', + ); + return { + ...newState, + routes: [ + ...newState.routes, + { + name: ConnectEthereumRouteName, + key: 'ReconnectEthereum', + params: action.payload.params, + }, + ], + index: newState.index + 1, + }; + } else { + return baseGetStateForAction(lastState, action, options); + } + }, + actionCreators: { + ...baseActionCreators, + reconnectEthereum: (params: ConnectEthereumParams) => ({ + type: reconnectEthereumActionType, + payload: { params }, + }), + }, + }; +} + +export default RegistrationRouter; diff --git a/native/account/registration/registration-terms.react.js b/native/account/registration/registration-terms.react.js index fe30ca695..2a72180ac 100644 --- a/native/account/registration/registration-terms.react.js +++ b/native/account/registration/registration-terms.react.js @@ -1,198 +1,192 @@ // @flow -import type { - StackNavigationEventMap, - StackNavigationState, - StackOptions, -} from '@react-navigation/core'; import invariant from 'invariant'; import * as React from 'react'; import { Text, View, Image, Linking, Alert } from 'react-native'; import type { SIWEBackupSecrets } from 'lib/types/siwe-types.js'; 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 commSwooshSource from '../../img/comm-swoosh.png'; import { logInActionType } from '../../navigation/action-types.js'; -import type { RootNavigationProp } from '../../navigation/root-navigator.react.js'; -import type { - NavigationRoute, - ScreenParamList, -} from '../../navigation/route-names.js'; +import type { NavigationRoute } from '../../navigation/route-names.js'; import { useStyles } from '../../themes/colors.js'; export type RegistrationTermsParams = { +userSelections: { +coolOrNerdMode?: ?CoolOrNerdMode, +keyserverURL?: ?string, +farcasterID: ?string, +accountSelection: AccountSelection, +avatarData: ?AvatarData, +siweBackupSecrets?: ?SIWEBackupSecrets, }, }; const onTermsOfUsePressed = () => { void Linking.openURL('https://comm.app/terms'); }; const onPrivacyPolicyPressed = () => { void Linking.openURL('https://comm.app/privacy'); }; type Props = { +navigation: RegistrationNavigationProp<'RegistrationTerms'>, +route: NavigationRoute<'RegistrationTerms'>, }; function RegistrationTerms(props: Props): React.Node { const registrationContext = React.useContext(RegistrationContext); invariant(registrationContext, 'registrationContext should be set'); const { register, setCachedSelections } = registrationContext; const [registrationInProgress, setRegistrationInProgress] = React.useState(false); const { userSelections } = props.route.params; const clearCachedSelections = React.useCallback(() => { setCachedSelections({}); }, [setCachedSelections]); const { navigation } = props; - const goBackToHome = navigation.getParent< - ScreenParamList, - 'Registration', - StackNavigationState, - StackOptions, - StackNavigationEventMap, - RootNavigationProp<'Registration'>, - >()?.goBack; + const { reconnectEthereum } = navigation; + const { coolOrNerdMode, keyserverURL, farcasterID } = userSelections; + const navigateToConnectEthereum = React.useCallback(() => { + reconnectEthereum({ + userSelections: { + coolOrNerdMode, + keyserverURL, + farcasterID, + }, + }); + }, [reconnectEthereum, coolOrNerdMode, keyserverURL, farcasterID]); const onNonceExpired = React.useCallback(() => { setCachedSelections(oldUserSelections => ({ ...oldUserSelections, ethereumAccount: undefined, })); Alert.alert( 'Registration attempt timed out', 'Please try to connect your Ethereum wallet again', - [{ text: 'OK', onPress: goBackToHome }], + [{ text: 'OK', onPress: navigateToConnectEthereum }], { cancelable: false, }, ); - }, [goBackToHome, setCachedSelections]); + }, [setCachedSelections, navigateToConnectEthereum]); const onProceed = React.useCallback(async () => { setRegistrationInProgress(true); try { await register({ ...userSelections, clearCachedSelections, onNonceExpired, }); } finally { setRegistrationInProgress(false); } }, [register, userSelections, clearCachedSelections, onNonceExpired]); React.useEffect(() => { if (!registrationInProgress) { return undefined; } navigation.setOptions({ gestureEnabled: false, headerLeft: null, }); const removeListener = navigation.addListener('beforeRemove', e => { if (e.data.action.type !== logInActionType) { e.preventDefault(); } }); return () => { navigation.setOptions({ gestureEnabled: true, headerLeft: undefined, }); removeListener(); }; }, [navigation, registrationInProgress]); const styles = useStyles(unboundStyles); const termsNotice = ( By registering, you are agreeing to our{' '} Terms of Use {' and '} Privacy Policy . ); return ( Finish registration {termsNotice} ); } const unboundStyles = { scrollViewContentContainer: { flexGrow: 1, }, header: { fontSize: 24, color: 'panelForegroundLabel', paddingBottom: 16, }, body: { fontFamily: 'Arial', fontSize: 15, lineHeight: 20, color: 'panelForegroundSecondaryLabel', paddingBottom: 16, }, commSwooshContainer: { flexGrow: 1, flexShrink: 1, alignItems: 'center', justifyContent: 'center', }, commSwoosh: { resizeMode: 'center', width: '100%', height: '100%', }, hyperlinkText: { color: 'purpleLink', }, }; export default RegistrationTerms; diff --git a/native/navigation/action-types.js b/native/navigation/action-types.js index c38584dfe..8adf204bd 100644 --- a/native/navigation/action-types.js +++ b/native/navigation/action-types.js @@ -1,17 +1,20 @@ // @flow // RootRouter export const logInActionType = 'LOG_IN'; export const logOutActionType = 'LOG_OUT'; export const clearRootModalsActionType = 'CLEAR_ROOT_MODALS'; export const setNavStateActionType = 'SET_NAV_STATE'; // OverlayRouter export const clearOverlayModalsActionType = 'CLEAR_OVERLAY_MODALS'; export const setRouteParamsActionType = 'SET_ROUTE_PARAMS'; // ChatRouter export const clearScreensActionType = 'CLEAR_SCREENS'; export const replaceWithThreadActionType = 'REPLACE_WITH_THREAD'; export const clearThreadsActionType = 'CLEAR_THREADS'; export const pushNewThreadActionType = 'PUSH_NEW_THREAD'; + +// RegistrationRouter +export const reconnectEthereumActionType = 'RECONNECT_ETHEREUM';