diff --git a/native/account/registration/connect-farcaster.react.js b/native/account/registration/connect-farcaster.react.js index 8d9d55ee4..3e908a951 100644 --- a/native/account/registration/connect-farcaster.react.js +++ b/native/account/registration/connect-farcaster.react.js @@ -1,228 +1,241 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { Alert } from 'react-native'; import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; import { useIsAppForegrounded } from 'lib/shared/lifecycle-utils.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 } from './registration-types.js'; import FarcasterPrompt from '../../components/farcaster-prompt.react.js'; import FarcasterWebView from '../../components/farcaster-web-view.react.js'; import type { FarcasterWebViewState } from '../../components/farcaster-web-view.react.js'; import { type NavigationRoute, ConnectEthereumRouteName, AvatarSelectionRouteName, } from '../../navigation/route-names.js'; +import { useStaffCanSee } from '../../utils/staff-utils.js'; export type ConnectFarcasterParams = ?{ +userSelections?: { +coolOrNerdMode?: CoolOrNerdMode, +keyserverURL?: string, }, }; type Props = { +navigation: RegistrationNavigationProp<'ConnectFarcaster'>, +route: NavigationRoute<'ConnectFarcaster'>, }; function ConnectFarcaster(prop: Props): React.Node { const { navigation, route } = prop; const { navigate } = navigation; const userSelections = route.params?.userSelections; const registrationContext = React.useContext(RegistrationContext); invariant(registrationContext, 'registrationContext should be set'); const { cachedSelections, setCachedSelections, skipEthereumLoginOnce, setSkipEthereumLoginOnce, } = registrationContext; const [webViewState, setWebViewState] = React.useState('closed'); const { ethereumAccount } = cachedSelections; const goToNextStep = React.useCallback( (fid?: ?string) => { setWebViewState('closed'); if (!skipEthereumLoginOnce || !ethereumAccount) { navigate<'ConnectEthereum'>({ name: ConnectEthereumRouteName, params: { userSelections: { ...userSelections, farcasterID: fid, }, }, }); return; } const newUserSelections = { ...userSelections, farcasterID: fid, accountSelection: ethereumAccount, }; setSkipEthereumLoginOnce(false); navigate<'AvatarSelection'>({ name: AvatarSelectionRouteName, params: { userSelections: newUserSelections }, }); }, [ navigate, skipEthereumLoginOnce, setSkipEthereumLoginOnce, ethereumAccount, userSelections, ], ); const onSkip = React.useCallback(() => goToNextStep(), [goToNextStep]); const identityServiceClient = React.useContext(IdentityClientContext); const getFarcasterUsers = identityServiceClient?.identityClient.getFarcasterUsers; invariant(getFarcasterUsers, 'Could not get getFarcasterUsers'); const [queuedAlert, setQueuedAlert] = React.useState(); const onSuccess = React.useCallback( async (fid: string) => { try { const commFCUsers = await getFarcasterUsers([fid]); if (commFCUsers.length > 0 && commFCUsers[0].farcasterID === fid) { const commUsername = commFCUsers[0].username; setQueuedAlert({ title: 'Farcaster account already linked', body: `That Farcaster account is already linked to ${commUsername}`, }); setWebViewState('closed'); } else { goToNextStep(fid); setCachedSelections(oldUserSelections => ({ ...oldUserSelections, farcasterID: fid, })); } } catch (e) { setQueuedAlert({ title: 'Failed to query Comm', body: 'We failed to query Comm to see if that Farcaster account is ' + 'already linked', }); setWebViewState('closed'); } }, [goToNextStep, setCachedSelections, getFarcasterUsers], ); const isAppForegrounded = useIsAppForegrounded(); React.useEffect(() => { if (!queuedAlert || !isAppForegrounded) { return; } Alert.alert(queuedAlert.title, queuedAlert.body); setQueuedAlert(null); }, [queuedAlert, isAppForegrounded]); const { farcasterID } = cachedSelections; const alreadyHasConnected = !!farcasterID; const onPressConnectFarcaster = React.useCallback(() => { setWebViewState('opening'); }, []); const defaultConnectButtonVariant = alreadyHasConnected ? 'outline' : 'enabled'; const connectButtonVariant = webViewState === 'opening' ? 'loading' : defaultConnectButtonVariant; const connectButtonText = alreadyHasConnected ? 'Connect new Farcaster account' : 'Connect Farcaster account'; const onUseAlreadyConnectedAccount = React.useCallback(() => { invariant( farcasterID, 'farcasterID should be set in onUseAlreadyConnectedAccount', ); goToNextStep(farcasterID); }, [farcasterID, goToNextStep]); const alreadyConnectedButton = React.useMemo(() => { if (!alreadyHasConnected) { return null; } return ( ); }, [alreadyHasConnected, onUseAlreadyConnectedAccount]); + const staffCanSee = useStaffCanSee(); + const skipButton = React.useMemo(() => { + if (!staffCanSee) { + return undefined; + } + return ( + + ); + }, [staffCanSee, onSkip]); + + const farcasterPromptTextType = staffCanSee ? 'optional' : 'required'; const connectFarcaster = React.useMemo( () => ( - + {alreadyConnectedButton} - + {skipButton} ), [ alreadyConnectedButton, connectButtonText, connectButtonVariant, onPressConnectFarcaster, - onSkip, onSuccess, webViewState, + farcasterPromptTextType, + skipButton, ], ); return connectFarcaster; } const styles = { scrollViewContentContainer: { flexGrow: 1, }, }; export default ConnectFarcaster; diff --git a/native/components/connect-farcaster-bottom-sheet.react.js b/native/components/connect-farcaster-bottom-sheet.react.js index 65ec39d1c..eaa9e9715 100644 --- a/native/components/connect-farcaster-bottom-sheet.react.js +++ b/native/components/connect-farcaster-bottom-sheet.react.js @@ -1,124 +1,124 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View, StyleSheet } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { setSyncedMetadataEntryActionType } from 'lib/actions/synced-metadata-actions.js'; import { useIsAppForegrounded } from 'lib/shared/lifecycle-utils.js'; import { syncedMetadataNames } from 'lib/types/synced-metadata-types.js'; import { useCurrentUserFID } from 'lib/utils/farcaster-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import FarcasterPrompt from './farcaster-prompt.react.js'; import FarcasterWebView, { type FarcasterWebViewState, } from './farcaster-web-view.react.js'; import RegistrationButton from '../account/registration/registration-button.react.js'; import { BottomSheetContext } from '../bottom-sheet/bottom-sheet-provider.react.js'; import BottomSheet from '../bottom-sheet/bottom-sheet.react.js'; import type { RootNavigationProp } from '../navigation/root-navigator.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; const bottomSheetPaddingTop = 32; const farcasterPromptHeight = 350; const marginBottom = 40; const buttonHeight = 48; type Props = { +navigation: RootNavigationProp<'ConnectFarcasterBottomSheet'>, +route: NavigationRoute<'ConnectFarcasterBottomSheet'>, }; function ConnectFarcasterBottomSheet(props: Props): React.Node { const { navigation } = props; const dispatch = useDispatch(); const fid = useCurrentUserFID(); const onSuccess = React.useCallback( (newFID: string) => { dispatch({ type: setSyncedMetadataEntryActionType, payload: { name: syncedMetadataNames.CURRENT_USER_FID, data: newFID, }, }); }, [dispatch], ); const { goBack } = navigation; const bottomSheetRef = React.useRef(null); const bottomSheetContext = React.useContext(BottomSheetContext); invariant(bottomSheetContext, 'bottomSheetContext should be set'); const { setContentHeight } = bottomSheetContext; const insets = useSafeAreaInsets(); React.useLayoutEffect(() => { setContentHeight( bottomSheetPaddingTop + farcasterPromptHeight + marginBottom + buttonHeight + insets.bottom, ); }, [insets.bottom, setContentHeight]); const [webViewState, setWebViewState] = React.useState('closed'); const isAppForegrounded = useIsAppForegrounded(); React.useEffect(() => { if (fid && isAppForegrounded) { bottomSheetRef.current?.close(); } }, [fid, isAppForegrounded]); const onPressConnect = React.useCallback(() => { setWebViewState('opening'); }, []); const connectButtonVariant = webViewState === 'opening' ? 'loading' : 'enabled'; const connectFarcasterBottomSheet = React.useMemo( () => ( - + ), [connectButtonVariant, goBack, onPressConnect, onSuccess, webViewState], ); return connectFarcasterBottomSheet; } const styles = StyleSheet.create({ container: { flex: 1, paddingHorizontal: 16, }, promptContainer: { marginBottom: 40, }, }); export default ConnectFarcasterBottomSheet; diff --git a/native/components/farcaster-prompt.react.js b/native/components/farcaster-prompt.react.js index 756fbaa9a..daabef0aa 100644 --- a/native/components/farcaster-prompt.react.js +++ b/native/components/farcaster-prompt.react.js @@ -1,70 +1,85 @@ // @flow import * as React from 'react'; import { View, Text } from 'react-native'; import { useStyles } from '../themes/colors.js'; import FarcasterLogo from '../vectors/farcaster-logo.react.js'; +type TextType = 'required' | 'optional' | 'disconnect'; + type Props = { - +showDisconnectText?: boolean, + +textType: TextType, }; function FarcasterPrompt(props: Props): React.Node { - const { showDisconnectText } = props; - - const styles = useStyles(unboundStyles); + const { textType } = props; - const headerText = showDisconnectText - ? 'Disconnect from Farcaster' - : 'Do you want to connect your Farcaster account?'; + let headerText; + if (textType === 'required') { + headerText = 'Connect your Farcaster account'; + } else if (textType === 'disconnect') { + headerText = 'Disconnect from Farcaster'; + } else { + headerText = 'Do you want to connect your Farcaster account?'; + } - const bodyText = showDisconnectText - ? 'You can disconnect your Farcaster account at any time.' - : 'Connecting your Farcaster account lets you see your mutual follows ' + - 'on Comm. We’ll also surface communities based on your Farcaster ' + + let bodyText; + if (textType === 'required') { + bodyText = + 'Connecting a Farcaster account is currently required. We use it to ' + + 'boostrap your social graph, and to surface communities based on your ' + + 'Farcaster channels.'; + } else if (textType === 'disconnect') { + bodyText = 'You can disconnect your Farcaster account at any time.'; + } else { + bodyText = + 'Connecting your Farcaster account lets us bootstrap your social ' + + 'graph. We’ll also surface communities based on your Farcaster ' + 'channels.'; + } + const styles = useStyles(unboundStyles); const farcasterPrompt = React.useMemo( () => ( <> {headerText} {bodyText} ), [ bodyText, headerText, styles.body, styles.farcasterLogoContainer, styles.header, ], ); return farcasterPrompt; } const unboundStyles = { header: { fontSize: 24, color: 'panelForegroundLabel', paddingBottom: 16, }, body: { fontFamily: 'Arial', fontSize: 15, lineHeight: 20, color: 'panelForegroundSecondaryLabel', paddingBottom: 16, }, farcasterLogoContainer: { flexGrow: 1, alignItems: 'center', justifyContent: 'center', }, }; export default FarcasterPrompt; diff --git a/native/profile/farcaster-account-settings.react.js b/native/profile/farcaster-account-settings.react.js index 642c1b405..c3b19f454 100644 --- a/native/profile/farcaster-account-settings.react.js +++ b/native/profile/farcaster-account-settings.react.js @@ -1,129 +1,130 @@ // @flow import * as React from 'react'; import { View } from 'react-native'; import { setSyncedMetadataEntryActionType, clearSyncedMetadataEntryActionType, } from 'lib/actions/synced-metadata-actions.js'; import { syncedMetadataNames } from 'lib/types/synced-metadata-types.js'; import { useCurrentUserFID } from 'lib/utils/farcaster-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import type { ProfileNavigationProp } from './profile.react.js'; import RegistrationButton from '../account/registration/registration-button.react.js'; import FarcasterPrompt from '../components/farcaster-prompt.react.js'; import FarcasterWebView from '../components/farcaster-web-view.react.js'; import type { FarcasterWebViewState } from '../components/farcaster-web-view.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { useStyles } from '../themes/colors.js'; type Props = { +navigation: ProfileNavigationProp<'FarcasterAccountSettings'>, +route: NavigationRoute<'FarcasterAccountSettings'>, }; // eslint-disable-next-line no-unused-vars function FarcasterAccountSettings(props: Props): React.Node { const dispatch = useDispatch(); const fid = useCurrentUserFID(); const styles = useStyles(unboundStyles); const onPressDisconnect = React.useCallback(() => { dispatch({ type: clearSyncedMetadataEntryActionType, payload: { name: syncedMetadataNames.CURRENT_USER_FID, }, }); }, [dispatch]); const [webViewState, setWebViewState] = React.useState('closed'); const onSuccess = React.useCallback( (newFID: string) => { setWebViewState('closed'); dispatch({ type: setSyncedMetadataEntryActionType, payload: { name: syncedMetadataNames.CURRENT_USER_FID, data: newFID, }, }); }, [dispatch], ); const onPressConnectFarcaster = React.useCallback(() => { setWebViewState('opening'); }, []); const connectButtonVariant = webViewState === 'opening' ? 'loading' : 'enabled'; const button = React.useMemo(() => { if (fid) { return ( ); } return ( ); }, [connectButtonVariant, fid, onPressConnectFarcaster, onPressDisconnect]); + const farcasterPromptTextType = fid ? 'disconnect' : 'optional'; const farcasterAccountSettings = React.useMemo( () => ( - + {button} ), [ button, - fid, + farcasterPromptTextType, onSuccess, styles.buttonContainer, styles.connectContainer, styles.promptContainer, webViewState, ], ); return farcasterAccountSettings; } const unboundStyles = { connectContainer: { flex: 1, backgroundColor: 'panelBackground', paddingBottom: 16, }, promptContainer: { flex: 1, padding: 16, justifyContent: 'space-between', }, buttonContainer: { marginVertical: 8, marginHorizontal: 16, }, }; export default FarcasterAccountSettings;