Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F32204510
D14984.1765115955.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
17 KB
Referenced Files
None
Subscribers
None
D14984.1765115955.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D14984: [native] Add an option to link DCs from a profile screen
Attached
Detach File
Event Timeline
Log In to Comment