diff --git a/lib/utils/farcaster-utils.js b/lib/utils/farcaster-utils.js index 714283a45..ab7b21bf4 100644 --- a/lib/utils/farcaster-utils.js +++ b/lib/utils/farcaster-utils.js @@ -1,15 +1,45 @@ // @flow +import invariant from 'invariant'; +import * as React from 'react'; + +import { setSyncedMetadataEntryActionType } from '../actions/synced-metadata-actions.js'; +import { IdentityClientContext } from '../shared/identity-client-context.js'; import { syncedMetadataNames } from '../types/synced-metadata-types.js'; -import { useSelector } from '../utils/redux-utils.js'; +import { useSelector, useDispatch } from '../utils/redux-utils.js'; function useCurrentUserFID(): ?string { return useSelector( state => state.syncedMetadataStore.syncedMetadata[ syncedMetadataNames.CURRENT_USER_FID ] ?? null, ); } -export { useCurrentUserFID }; +function useLinkFID(): (fid: string) => Promise { + const identityClientContext = React.useContext(IdentityClientContext); + invariant(identityClientContext, 'identityClientContext should be set'); + + const { identityClient } = identityClientContext; + const { linkFarcasterAccount } = identityClient; + + const dispatch = useDispatch(); + + return React.useCallback( + async (fid: string) => { + await linkFarcasterAccount(fid); + + dispatch({ + type: setSyncedMetadataEntryActionType, + payload: { + name: syncedMetadataNames.CURRENT_USER_FID, + data: fid, + }, + }); + }, + [dispatch, linkFarcasterAccount], + ); +} + +export { useCurrentUserFID, useLinkFID }; diff --git a/native/components/connect-farcaster-bottom-sheet.react.js b/native/components/connect-farcaster-bottom-sheet.react.js index eaa9e9715..cad814f4f 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'; +import { useTryLinkFID } from '../utils/farcaster-utils.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 { goBack } = navigation; + + const [webViewState, setWebViewState] = + React.useState('closed'); + + const [isLoadingLinkFID, setIsLoadingLinkFID] = React.useState(false); const fid = useCurrentUserFID(); + const tryLinkFID = useTryLinkFID(); + const onSuccess = React.useCallback( - (newFID: string) => { - dispatch({ - type: setSyncedMetadataEntryActionType, - payload: { - name: syncedMetadataNames.CURRENT_USER_FID, - data: newFID, - }, - }); + async (newFID: string) => { + setWebViewState('closed'); + + try { + await tryLinkFID(newFID); + } finally { + setIsLoadingLinkFID(false); + } }, - [dispatch], + [tryLinkFID], ); - 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(() => { + setIsLoadingLinkFID(true); setWebViewState('opening'); }, []); - const connectButtonVariant = - webViewState === 'opening' ? 'loading' : 'enabled'; + const connectButtonVariant = isLoadingLinkFID ? '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/profile/farcaster-account-settings.react.js b/native/profile/farcaster-account-settings.react.js index c3b19f454..e0809877a 100644 --- a/native/profile/farcaster-account-settings.react.js +++ b/native/profile/farcaster-account-settings.react.js @@ -1,130 +1,131 @@ // @flow import * as React from 'react'; import { View } from 'react-native'; -import { - setSyncedMetadataEntryActionType, - clearSyncedMetadataEntryActionType, -} from 'lib/actions/synced-metadata-actions.js'; +import { 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'; +import { useTryLinkFID } from '../utils/farcaster-utils.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 [isLoadingLinkFID, setIsLoadingLinkFID] = React.useState(false); + + const tryLinkFID = useTryLinkFID(); + const onSuccess = React.useCallback( - (newFID: string) => { + async (newFID: string) => { setWebViewState('closed'); - dispatch({ - type: setSyncedMetadataEntryActionType, - payload: { - name: syncedMetadataNames.CURRENT_USER_FID, - data: newFID, - }, - }); + + try { + await tryLinkFID(newFID); + } finally { + setIsLoadingLinkFID(false); + } }, - [dispatch], + [tryLinkFID], ); const onPressConnectFarcaster = React.useCallback(() => { + setIsLoadingLinkFID(true); setWebViewState('opening'); }, []); - const connectButtonVariant = - webViewState === 'opening' ? 'loading' : 'enabled'; + const connectButtonVariant = isLoadingLinkFID ? '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, 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; diff --git a/native/utils/farcaster-utils.js b/native/utils/farcaster-utils.js new file mode 100644 index 000000000..b32933397 --- /dev/null +++ b/native/utils/farcaster-utils.js @@ -0,0 +1,41 @@ +// @flow + +import * as React from 'react'; +import { Alert } from 'react-native'; + +import { getMessageForException } from 'lib/utils/errors.js'; +import { useLinkFID } from 'lib/utils/farcaster-utils.js'; + +import { + getFarcasterAccountAlreadyLinkedAlertDetails, + UnknownErrorAlertDetails, +} from './alert-messages.js'; + +function useTryLinkFID(): (newFID: string) => Promise { + const linkFID = useLinkFID(); + + return React.useCallback( + async (newFID: string) => { + try { + await linkFID(newFID); + } catch (e) { + if ( + getMessageForException(e) === + 'farcaster ID already associated with different user' + ) { + const { title, message } = + getFarcasterAccountAlreadyLinkedAlertDetails(); + Alert.alert(title, message); + } else { + Alert.alert( + UnknownErrorAlertDetails.title, + UnknownErrorAlertDetails.message, + ); + } + } + }, + [linkFID], + ); +} + +export { useTryLinkFID };