Page MenuHomePhorge

D14984.1765115955.diff
No OneTemporary

Size
17 KB
Referenced Files
None
Subscribers
None

D14984.1765115955.diff

diff --git a/lib/types/synced-metadata-types.js b/lib/types/synced-metadata-types.js
--- a/lib/types/synced-metadata-types.js
+++ b/lib/types/synced-metadata-types.js
@@ -8,6 +8,7 @@
const syncedMetadataNames = Object.freeze({
CURRENT_USER_FID: 'current_user_fid',
+ CURRENT_USER_SUPPORTS_DCS: 'current_user_supports_dcs',
STORE_VERSION: 'store_version',
ENABLED_APPS: 'enabled_apps',
GLOBAL_THEME_INFO: 'global_theme_info',
diff --git a/lib/utils/farcaster-utils.js b/lib/utils/farcaster-utils.js
--- a/lib/utils/farcaster-utils.js
+++ b/lib/utils/farcaster-utils.js
@@ -3,14 +3,15 @@
import invariant from 'invariant';
import * as React from 'react';
+import { useSelector, useDispatch } from './redux-utils.js';
import { setSyncedMetadataEntryActionType } from '../actions/synced-metadata-actions.js';
import { useUserIdentityCache } from '../components/user-identity-cache.react.js';
import { IdentityClientContext } from '../shared/identity-client-context.js';
import { syncedMetadataNames } from '../types/synced-metadata-types.js';
-import { useSelector, useDispatch } from '../utils/redux-utils.js';
const DISABLE_CONNECT_FARCASTER_ALERT = false;
const NO_FID_METADATA = 'NONE';
+const NO_DCS_SUPPORT_METADATA = 'NONE';
function useCurrentUserFID(): ?string {
// There is a distinction between null & undefined for the fid value.
@@ -31,6 +32,25 @@
return currentUserFID;
}
+function useCurrentUserSupportsDCs(): ?boolean {
+ // There is a distinction between null & undefined for the fid DCs value.
+ // If the fid DCs is null this means that the user has decided NOT to set
+ // a Farcaster DCs association. If the fid DCs is undefined this means that
+ // the user has not yet been prompted to set a Farcaster DCs association.
+ const currentUserFIDDCs = useSelector(
+ state =>
+ state.syncedMetadataStore.syncedMetadata[
+ syncedMetadataNames.CURRENT_USER_SUPPORTS_DCS
+ ] ?? undefined,
+ );
+
+ if (currentUserFIDDCs === NO_DCS_SUPPORT_METADATA) {
+ return null;
+ }
+
+ return currentUserFIDDCs === 'true';
+}
+
function useSetLocalFID(): (fid: ?string) => void {
const dispatch = useDispatch();
const { invalidateCacheForUser } = useUserIdentityCache();
@@ -55,6 +75,31 @@
);
}
+function useSetLocalCurrentUserSupportsDCs(): (connected: ?boolean) => void {
+ const dispatch = useDispatch();
+ const { invalidateCacheForUser } = useUserIdentityCache();
+ const currentUserID = useSelector(state => state.currentUserInfo?.id);
+ return React.useCallback(
+ (connected: ?boolean) => {
+ // If we're unsetting the DCs support, we should set it to
+ // NO_DCS_SUPPORT_METADATA to avoid prompting the user for it again
+ const connectionStatus =
+ connected === null ? NO_DCS_SUPPORT_METADATA : String(connected);
+ dispatch({
+ type: setSyncedMetadataEntryActionType,
+ payload: {
+ name: syncedMetadataNames.CURRENT_USER_SUPPORTS_DCS,
+ data: connectionStatus,
+ },
+ });
+ if (currentUserID) {
+ invalidateCacheForUser(currentUserID);
+ }
+ },
+ [dispatch, currentUserID, invalidateCacheForUser],
+ );
+}
+
function useLinkFID(): (fid: string) => Promise<void> {
const identityClientContext = React.useContext(IdentityClientContext);
invariant(identityClientContext, 'identityClientContext should be set');
@@ -81,11 +126,34 @@
const { unlinkFarcasterAccount } = identityClient;
const setLocalFID = useSetLocalFID();
+ const setLocalDCsSupport = useSetLocalCurrentUserSupportsDCs();
return React.useCallback(async () => {
await unlinkFarcasterAccount();
setLocalFID(null);
- }, [setLocalFID, unlinkFarcasterAccount]);
+ setLocalDCsSupport(null);
+ }, [setLocalFID, setLocalDCsSupport, unlinkFarcasterAccount]);
+}
+
+function useLinkFarcasterDCs(): (
+ fid: string,
+ farcasterDCsToken: string,
+) => Promise<void> {
+ const identityClientContext = React.useContext(IdentityClientContext);
+ invariant(identityClientContext, 'identityClientContext should be set');
+
+ const { identityClient } = identityClientContext;
+ const { linkFarcasterDCsAccount } = identityClient;
+
+ const setLocalDCsSupport = useSetLocalCurrentUserSupportsDCs();
+
+ return React.useCallback(
+ async (fid: string, farcasterDCsToken: string) => {
+ await linkFarcasterDCsAccount(fid, farcasterDCsToken);
+ setLocalDCsSupport(true);
+ },
+ [setLocalDCsSupport, linkFarcasterDCsAccount],
+ );
}
function createFarcasterDCsAuthMessage(fid: string, nonce: string): string {
@@ -112,9 +180,13 @@
export {
DISABLE_CONNECT_FARCASTER_ALERT,
NO_FID_METADATA,
+ NO_DCS_SUPPORT_METADATA,
useCurrentUserFID,
+ useCurrentUserSupportsDCs,
useSetLocalFID,
+ useSetLocalCurrentUserSupportsDCs,
useLinkFID,
useUnlinkFID,
+ useLinkFarcasterDCs,
createFarcasterDCsAuthMessage,
};
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
@@ -17,7 +17,10 @@
type LogOutResult,
} from 'lib/types/account-types.js';
import { getMessageForException } from 'lib/utils/errors.js';
-import { useSetLocalFID } from 'lib/utils/farcaster-utils.js';
+import {
+ useSetLocalCurrentUserSupportsDCs,
+ useSetLocalFID,
+} from 'lib/utils/farcaster-utils.js';
import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js';
import { useDispatch } from 'lib/utils/redux-utils.js';
import { setURLPrefix } from 'lib/utils/url-utils.js';
@@ -189,6 +192,7 @@
const dispatch = useDispatch();
const setLocalFID = useSetLocalFID();
+ const setLocalDCsSupport = useSetLocalCurrentUserSupportsDCs();
const returnedFunc = React.useCallback(
(input: RegistrationServerCallInput) =>
new Promise<void>(
@@ -232,6 +236,7 @@
});
}
setLocalFID(farcasterID);
+ setLocalDCsSupport(!!farcasterDCsToken);
if (siweBackupSecrets) {
await commCoreModule.setSIWEBackupSecrets(siweBackupSecrets);
}
@@ -261,6 +266,7 @@
dispatch,
identityRegisterEthereumAccount,
identityRegisterUsernameAccount,
+ setLocalDCsSupport,
setLocalFID,
],
);
diff --git a/native/components/farcaster-prompt.react.js b/native/components/farcaster-prompt.react.js
--- a/native/components/farcaster-prompt.react.js
+++ b/native/components/farcaster-prompt.react.js
@@ -6,7 +6,11 @@
import { useStyles } from '../themes/colors.js';
import FarcasterLogo from '../vectors/farcaster-logo.react.js';
-type TextType = 'connect' | 'disconnect' | 'connect_DC';
+type TextType =
+ | 'connect'
+ | 'disconnect'
+ | 'disconnect_or_connect_DC'
+ | 'connect_DC';
type Props = {
+textType: TextType,
@@ -27,6 +31,11 @@
bodyTexts: ['You can disconnect your Farcaster account at any time.'],
displayLogo: true,
},
+ disconnect_or_connect_DC: {
+ headerText: 'Farcaster account',
+ bodyTexts: ['Your Farcaster account is connected.'],
+ displayLogo: true,
+ },
connect_DC: {
headerText: 'Do you want to connect your Farcaster Direct Casts?',
bodyTexts: [
diff --git a/native/profile/connect-farcaster-dcs.react.js b/native/profile/connect-farcaster-dcs.react.js
new file mode 100644
--- /dev/null
+++ b/native/profile/connect-farcaster-dcs.react.js
@@ -0,0 +1,151 @@
+// @flow
+
+import * as React from 'react';
+import { ScrollView, View } from 'react-native';
+
+import {
+ useCurrentUserFID,
+ useLinkFarcasterDCs,
+} from 'lib/utils/farcaster-utils.js';
+
+import RegistrationTextInput from '../account/registration/registration-text-input.react.js';
+import FarcasterPrompt from '../components/farcaster-prompt.react.js';
+import PrimaryButton from '../components/primary-button.react.js';
+import { FarcasterAuthContextProvider } from '../farcaster-auth/farcaster-auth-context-provider.react.js';
+import { useGetAuthToken } from '../farcaster-auth/farcaster-auth-utils.js';
+import { useStyles } from '../themes/colors.js';
+import Alert from '../utils/alert.js';
+
+type Props = {
+ +onSuccess: () => void,
+ +onCancel: () => void,
+};
+
+function InnerConnectFarcasterDCs(props: Props): React.Node {
+ const { onSuccess, onCancel } = props;
+
+ const [mnemonic, setMnemonic] = React.useState<?string>(null);
+ const [signingInProgress, setSigningInProgress] = React.useState(false);
+
+ const scrollViewRef =
+ React.useRef<?React.ElementRef<typeof ScrollView>>(null);
+
+ const fid = useCurrentUserFID();
+ const getAuthToken = useGetAuthToken();
+ const linkDCs = useLinkFarcasterDCs();
+
+ const onConnect = React.useCallback(async () => {
+ if (!mnemonic || !fid) {
+ return;
+ }
+
+ setSigningInProgress(true);
+ try {
+ const token = await getAuthToken(fid, mnemonic);
+ await linkDCs(fid, token);
+ onSuccess();
+ } catch (e) {
+ Alert.alert(
+ 'Failed to connect',
+ 'Failed to connect to Farcaster Direct Casts. Please try again later.',
+ );
+ }
+ setSigningInProgress(false);
+ }, [getAuthToken, linkDCs, mnemonic, fid, onSuccess]);
+
+ const onChangeMnemonicText = React.useCallback((text: string) => {
+ setMnemonic(text);
+ }, []);
+
+ const onInputFocus = React.useCallback(() => {
+ // Scroll to make the input fully visible when focused
+ setTimeout(() => {
+ if (scrollViewRef.current) {
+ scrollViewRef.current.scrollToEnd({ animated: true });
+ }
+ }, 100);
+ }, []);
+
+ let buttonVariant = 'enabled';
+ if (!mnemonic) {
+ buttonVariant = 'disabled';
+ } else if (signingInProgress) {
+ buttonVariant = 'loading';
+ }
+
+ const styles = useStyles(unboundStyles);
+
+ return (
+ <View style={styles.container}>
+ <ScrollView
+ ref={scrollViewRef}
+ style={styles.scrollView}
+ contentContainerStyle={styles.scrollViewContent}
+ keyboardShouldPersistTaps="handled"
+ >
+ <View style={styles.contentContainer}>
+ <FarcasterPrompt textType="connect_DC" />
+ <View style={styles.inputContainer}>
+ <RegistrationTextInput
+ autoCapitalize="none"
+ autoComplete="off"
+ autoCorrect={false}
+ editable={!signingInProgress}
+ keyboardType="default"
+ onChangeText={onChangeMnemonicText}
+ onFocus={onInputFocus}
+ onSubmitEditing={onConnect}
+ placeholder="Wallet mnemonic"
+ returnKeyType="go"
+ secureTextEntry={true}
+ value={mnemonic}
+ />
+ </View>
+ </View>
+ </ScrollView>
+ <View style={styles.buttonContainer}>
+ <PrimaryButton
+ onPress={onConnect}
+ label="Connect Direct Casts"
+ variant={buttonVariant}
+ />
+ <PrimaryButton onPress={onCancel} label="Cancel" variant="outline" />
+ </View>
+ </View>
+ );
+}
+
+const unboundStyles = {
+ container: {
+ flex: 1,
+ backgroundColor: 'panelBackground',
+ paddingBottom: 16,
+ },
+ scrollView: {
+ flex: 1,
+ },
+ scrollViewContent: {
+ flexGrow: 1,
+ },
+ contentContainer: {
+ flex: 1,
+ padding: 16,
+ },
+ inputContainer: {
+ marginTop: 16,
+ },
+ buttonContainer: {
+ marginVertical: 8,
+ marginHorizontal: 16,
+ },
+};
+
+function ConnectFarcasterDCs(props: Props): React.Node {
+ return (
+ <FarcasterAuthContextProvider>
+ <InnerConnectFarcasterDCs {...props} />
+ </FarcasterAuthContextProvider>
+ );
+}
+
+export default ConnectFarcasterDCs;
diff --git a/native/profile/farcaster-account-settings.react.js b/native/profile/farcaster-account-settings.react.js
--- a/native/profile/farcaster-account-settings.react.js
+++ b/native/profile/farcaster-account-settings.react.js
@@ -1,14 +1,20 @@
// @flow
import * as React from 'react';
-import { View } from 'react-native';
+import { ScrollView, View } from 'react-native';
-import { useCurrentUserFID, useUnlinkFID } from 'lib/utils/farcaster-utils.js';
+import {
+ useCurrentUserFID,
+ useCurrentUserSupportsDCs,
+ useUnlinkFID,
+} from 'lib/utils/farcaster-utils.js';
+import { supportsFarcasterDCs } from 'lib/utils/services-utils.js';
+import ConnectFarcasterDCs from './connect-farcaster-dcs.react.js';
import type { ProfileNavigationProp } from './profile.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 FarcasterWebView from '../components/farcaster-web-view.react.js';
import PrimaryButton from '../components/primary-button.react.js';
import type { NavigationRoute } from '../navigation/route-names.js';
import { useStyles } from '../themes/colors.js';
@@ -24,6 +30,7 @@
// eslint-disable-next-line no-unused-vars
function FarcasterAccountSettings(props: Props): React.Node {
const fid = useCurrentUserFID();
+ const currentUserSupportsDCs = useCurrentUserSupportsDCs();
const styles = useStyles(unboundStyles);
@@ -47,6 +54,7 @@
const [webViewState, setWebViewState] =
React.useState<FarcasterWebViewState>('closed');
+ const [showConnectDCs, setShowConnectDCs] = React.useState(false);
const [isLoadingLinkFID, setIsLoadingLinkFID] = React.useState(false);
@@ -70,59 +78,111 @@
setWebViewState('opening');
}, []);
+ const onPressConnectDCs = React.useCallback(() => {
+ setShowConnectDCs(true);
+ }, []);
+
+ const onConnectDCsSuccess = React.useCallback(() => {
+ setShowConnectDCs(false);
+ }, []);
+
+ const onConnectDCsCancel = React.useCallback(() => {
+ setShowConnectDCs(false);
+ }, []);
+
const disconnectButtonVariant = isLoadingUnlinkFID ? 'loading' : 'outline';
const connectButtonVariant = isLoadingLinkFID ? 'loading' : 'enabled';
- const button = React.useMemo(() => {
+ const buttons = React.useMemo(() => {
if (fid) {
- return (
+ const buttonList = [
<PrimaryButton
+ key="disconnect"
onPress={onPressDisconnect}
label="Disconnect"
variant={disconnectButtonVariant}
- />
- );
+ />,
+ ];
+
+ if (supportsFarcasterDCs && !currentUserSupportsDCs) {
+ buttonList.unshift(
+ <PrimaryButton
+ key="connect-dcs"
+ onPress={onPressConnectDCs}
+ label="Connect Direct Casts"
+ variant="enabled"
+ />,
+ );
+ }
+
+ return buttonList;
}
- return (
+ return [
<PrimaryButton
+ key="connect"
onPress={onPressConnectFarcaster}
label="Connect Farcaster account"
variant={connectButtonVariant}
- />
- );
+ />,
+ ];
}, [
connectButtonVariant,
disconnectButtonVariant,
fid,
+ currentUserSupportsDCs,
onPressConnectFarcaster,
onPressDisconnect,
+ onPressConnectDCs,
]);
- const farcasterPromptTextType = fid ? 'disconnect' : 'connect';
- const farcasterAccountSettings = React.useMemo(
- () => (
+ const farcasterPromptTextType = React.useMemo(() => {
+ if (!fid) {
+ return 'connect';
+ }
+ if (supportsFarcasterDCs && !currentUserSupportsDCs) {
+ return 'disconnect_or_connect_DC';
+ }
+ return 'disconnect';
+ }, [fid, currentUserSupportsDCs]);
+
+ return React.useMemo(() => {
+ if (showConnectDCs) {
+ return (
+ <ConnectFarcasterDCs
+ onSuccess={onConnectDCsSuccess}
+ onCancel={onConnectDCsCancel}
+ />
+ );
+ }
+
+ return (
<View style={styles.connectContainer}>
- <View style={styles.promptContainer}>
+ <ScrollView
+ contentContainerStyle={styles.scrollViewContentContainer}
+ style={styles.promptContainer}
+ alwaysBounceVertical={false}
+ >
<FarcasterPrompt textType={farcasterPromptTextType} />
- </View>
+ </ScrollView>
+ <View style={styles.buttonContainer}>{buttons}</View>
<FarcasterWebView onSuccess={onSuccess} webViewState={webViewState} />
- <View style={styles.buttonContainer}>{button}</View>
</View>
- ),
- [
- button,
- farcasterPromptTextType,
- onSuccess,
- styles.buttonContainer,
- styles.connectContainer,
- styles.promptContainer,
- webViewState,
- ],
- );
-
- return farcasterAccountSettings;
+ );
+ }, [
+ buttons,
+ farcasterPromptTextType,
+ onConnectDCsCancel,
+ onConnectDCsSuccess,
+ onSuccess,
+ showConnectDCs,
+ styles.buttonContainer,
+ styles.connectContainer,
+ styles.promptContainer,
+ styles.scrollViewContentContainer,
+ webViewState,
+ ]);
}
const unboundStyles = {
@@ -130,16 +190,18 @@
flex: 1,
backgroundColor: 'panelBackground',
paddingBottom: 16,
+ justifyContent: 'space-between',
},
promptContainer: {
- flex: 1,
padding: 16,
- justifyContent: 'space-between',
},
buttonContainer: {
marginVertical: 8,
marginHorizontal: 16,
},
+ scrollViewContentContainer: {
+ padding: 16,
+ },
};
export default FarcasterAccountSettings;

File Metadata

Mime Type
text/plain
Expires
Sun, Dec 7, 1:59 PM (14 h, 10 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5844427
Default Alt Text
D14984.1765115955.diff (17 KB)

Event Timeline