Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F33084163
D14911.1768455876.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
21 KB
Referenced Files
None
Subscribers
None
D14911.1768455876.diff
View Options
diff --git a/native/.eslintrc.json b/native/.eslintrc.json
--- a/native/.eslintrc.json
+++ b/native/.eslintrc.json
@@ -1,6 +1,7 @@
{
"env": {
- "react-native/react-native": true
+ "react-native/react-native": true,
+ "browser": true
},
"plugins": ["react-native"],
"rules": {
diff --git a/native/account/registration/auth-navigator.react.js b/native/account/registration/auth-navigator.react.js
--- a/native/account/registration/auth-navigator.react.js
+++ b/native/account/registration/auth-navigator.react.js
@@ -25,6 +25,7 @@
} from './auth-router.js';
import AvatarSelection from './avatar-selection.react.js';
import ConnectEthereum from './connect-ethereum.react.js';
+import { ConnectFarcasterDCs } from './connect-farcaster-dc.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';
@@ -58,6 +59,7 @@
RestorePasswordAccountScreenRouteName,
RestoreBackupScreenRouteName,
RestoreSIWEBackupRouteName,
+ ConnectFarcasterDCsRouteName,
} from '../../navigation/route-names.js';
import QRCodeScreen from '../qr-code-screen.react.js';
import RestoreBackupScreen from '../restore-backup-screen.react.js';
@@ -227,6 +229,10 @@
name={RestoreSIWEBackupRouteName}
component={RestoreSIWEBackup}
/>
+ <Auth.Screen
+ name={ConnectFarcasterDCsRouteName}
+ component={ConnectFarcasterDCs}
+ />
</Auth.Navigator>
);
}
diff --git a/native/account/registration/avatar-selection.react.js b/native/account/registration/avatar-selection.react.js
--- a/native/account/registration/avatar-selection.react.js
+++ b/native/account/registration/avatar-selection.react.js
@@ -41,6 +41,7 @@
+accountSelection: AccountSelection,
+farcasterID: ?string,
+farcasterAvatarURL: ?string,
+ +farcasterDCsToken: ?string,
},
};
diff --git a/native/account/registration/connect-ethereum.react.js b/native/account/registration/connect-ethereum.react.js
--- a/native/account/registration/connect-ethereum.react.js
+++ b/native/account/registration/connect-ethereum.react.js
@@ -48,6 +48,7 @@
+keyserverURL?: ?string,
+farcasterID: ?string,
+farcasterAvatarURL: ?string,
+ +farcasterDCsToken: ?string,
},
};
diff --git a/native/account/registration/connect-farcaster-dc.react.js b/native/account/registration/connect-farcaster-dc.react.js
new file mode 100644
--- /dev/null
+++ b/native/account/registration/connect-farcaster-dc.react.js
@@ -0,0 +1,258 @@
+// @flow
+
+import invariant from 'invariant';
+import * as React from 'react';
+import { Text } from 'react-native';
+
+import type { AuthNavigationProp } from './auth-navigator.react.js';
+import { siweNonceExpired } from './ethereum-utils.js';
+import { RegistrationContext } from './registration-context.js';
+import RegistrationTextInput from './registration-text-input.react.js';
+import type { CoolOrNerdMode } from './registration-types.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 type { NavigationRoute } from '../../navigation/route-names.js';
+import {
+ AvatarSelectionRouteName,
+ ConnectEthereumRouteName,
+} from '../../navigation/route-names.js';
+import { useStyles } from '../../themes/colors.js';
+import Alert from '../../utils/alert.js';
+import AuthButtonContainer from '../auth-components/auth-button-container.react.js';
+import AuthContainer from '../auth-components/auth-container.react.js';
+import AuthContentContainer from '../auth-components/auth-content-container.react.js';
+
+export type ConnectFarcasterDCsParams = {
+ +userSelections: {
+ +coolOrNerdMode?: ?CoolOrNerdMode,
+ +keyserverURL?: ?string,
+ +farcasterID: string,
+ +farcasterAvatarURL: ?string,
+ },
+};
+
+type Props = {
+ +navigation: AuthNavigationProp<'ConnectFarcasterDCs'>,
+ +route: NavigationRoute<'ConnectFarcasterDCs'>,
+};
+
+function InnerConnectFarcasterDCs(props: Props): React.Node {
+ const { navigation, route } = props;
+
+ const { navigate } = navigation;
+ const userSelections = route.params?.userSelections;
+
+ const [mnemonic, setMnemonic] = React.useState<?string>(null);
+
+ const registrationContext = React.useContext(RegistrationContext);
+ invariant(registrationContext, 'registrationContext should be set');
+ const {
+ cachedSelections,
+ setCachedSelections,
+ skipEthereumLoginOnce,
+ setSkipEthereumLoginOnce,
+ } = registrationContext;
+
+ const { ethereumAccount } = cachedSelections;
+ const goToNextStep = React.useCallback(
+ (farcasterDCsToken?: ?string) => {
+ invariant(
+ !ethereumAccount || ethereumAccount.nonceTimestamp,
+ 'nonceTimestamp must be set after connecting to Ethereum account',
+ );
+ const nonceExpired =
+ ethereumAccount &&
+ ethereumAccount.nonceTimestamp &&
+ siweNonceExpired(ethereumAccount.nonceTimestamp);
+ if (nonceExpired) {
+ setCachedSelections(oldUserSelections => ({
+ ...oldUserSelections,
+ ethereumAccount: undefined,
+ }));
+ }
+
+ if (!skipEthereumLoginOnce || !ethereumAccount || nonceExpired) {
+ navigate<'ConnectEthereum'>({
+ name: ConnectEthereumRouteName,
+ params: {
+ userSelections: {
+ ...userSelections,
+ farcasterDCsToken,
+ },
+ },
+ });
+ return;
+ }
+
+ const newUserSelections = {
+ ...userSelections,
+ accountSelection: ethereumAccount,
+ farcasterDCsToken,
+ };
+ setSkipEthereumLoginOnce(false);
+ navigate<'AvatarSelection'>({
+ name: AvatarSelectionRouteName,
+ params: { userSelections: newUserSelections },
+ });
+ },
+ [
+ ethereumAccount,
+ navigate,
+ setCachedSelections,
+ setSkipEthereumLoginOnce,
+ skipEthereumLoginOnce,
+ userSelections,
+ ],
+ );
+
+ const onSkip = React.useCallback(() => {
+ if (cachedSelections.farcasterDCsToken) {
+ setCachedSelections(({ farcasterDCsToken, ...rest }) => rest);
+ }
+ goToNextStep();
+ }, [cachedSelections.farcasterDCsToken, goToNextStep, setCachedSelections]);
+
+ const getAuthToken = useGetAuthToken();
+ const [signingInProgress, setSigningInProgress] = React.useState(false);
+ const onConnect = React.useCallback(async () => {
+ if (!mnemonic) {
+ goToNextStep();
+ return;
+ }
+
+ setSigningInProgress(true);
+ try {
+ const token = await getAuthToken(userSelections.farcasterID, mnemonic);
+ setCachedSelections(oldUserSelections => ({
+ farcasterDCsToken: token,
+ ...oldUserSelections,
+ }));
+ goToNextStep(token);
+ } catch (e) {
+ Alert.alert(
+ 'Failed to connect',
+ 'Failed to connect to Farcaster Direct Casts. Please try again later.',
+ );
+ }
+ setSigningInProgress(false);
+ }, [
+ getAuthToken,
+ goToNextStep,
+ mnemonic,
+ setCachedSelections,
+ userSelections.farcasterID,
+ ]);
+
+ let buttonVariant = 'enabled';
+ if (!mnemonic || cachedSelections.farcasterDCsToken) {
+ buttonVariant = 'disabled';
+ } else if (signingInProgress) {
+ buttonVariant = 'loading';
+ }
+
+ const onUseAlreadyConnectedAccount = React.useCallback(() => {
+ goToNextStep(cachedSelections.farcasterDCsToken);
+ }, [cachedSelections.farcasterDCsToken, goToNextStep]);
+ const alreadyConnected = !!cachedSelections.farcasterDCsToken;
+ let alreadyConnectedButton = null;
+ if (alreadyConnected) {
+ alreadyConnectedButton = (
+ <PrimaryButton
+ onPress={onUseAlreadyConnectedAccount}
+ label="Use connected account"
+ variant="enabled"
+ />
+ );
+ }
+
+ const onChangeText = React.useCallback(
+ (text: string) => {
+ setMnemonic(text);
+ setCachedSelections(({ farcasterDCsToken, ...rest }) => rest);
+ },
+ [setCachedSelections],
+ );
+
+ const styles = useStyles(unboundStyles);
+ return React.useMemo(
+ () => (
+ <AuthContainer>
+ <AuthContentContainer style={styles.scrollViewContentContainer}>
+ <FarcasterPrompt textType="connect_DC" />
+ <Text style={styles.description}>
+ To connect your Farcaster Direct Casts, open your Farcaster app, go
+ to Settings → Advanced → Show Farcaster recovery phase, and copy
+ your 24-word mnemonic phrase. Paste it into the text field below.
+ </Text>
+ <Text style={styles.description}>
+ We’ll use this to sign a message with Farcaster and receive your
+ access token. Your mnemonic phrase is only used locally for signing
+ and is not stored on our servers.
+ </Text>
+ <RegistrationTextInput
+ autoCapitalize="none"
+ autoComplete="off"
+ autoCorrect={false}
+ editable={!signingInProgress}
+ keyboardType="default"
+ onChangeText={onChangeText}
+ onSubmitEditing={onConnect}
+ placeholder="Wellet mnemonic"
+ returnKeyType="go"
+ secureTextEntry={true}
+ value={mnemonic}
+ />
+ </AuthContentContainer>
+ <AuthButtonContainer>
+ {alreadyConnectedButton}
+ <PrimaryButton
+ onPress={onConnect}
+ label="Connect Direct Casts"
+ variant={buttonVariant}
+ />
+ <PrimaryButton
+ onPress={onSkip}
+ label="Do not connect"
+ variant="outline"
+ />
+ </AuthButtonContainer>
+ </AuthContainer>
+ ),
+ [
+ alreadyConnectedButton,
+ buttonVariant,
+ mnemonic,
+ onChangeText,
+ onConnect,
+ onSkip,
+ signingInProgress,
+ styles.description,
+ styles.scrollViewContentContainer,
+ ],
+ );
+}
+
+const unboundStyles = {
+ scrollViewContentContainer: {
+ flexGrow: 1,
+ },
+ description: {
+ fontFamily: 'Arial',
+ fontSize: 15,
+ lineHeight: 20,
+ color: 'panelForegroundSecondaryLabel',
+ paddingBottom: 16,
+ },
+};
+
+function ConnectFarcasterDCs(props: Props): React.Node {
+ return (
+ <FarcasterAuthContextProvider>
+ <InnerConnectFarcasterDCs {...props} />
+ </FarcasterAuthContextProvider>
+ );
+}
+
+export { ConnectFarcasterDCs };
diff --git a/native/account/registration/connect-farcaster.react.js b/native/account/registration/connect-farcaster.react.js
--- a/native/account/registration/connect-farcaster.react.js
+++ b/native/account/registration/connect-farcaster.react.js
@@ -64,6 +64,21 @@
const goToNextStep = React.useCallback(
(fid?: ?string, farcasterAvatarURL: ?string) => {
setWebViewState('closed');
+
+ if (fid) {
+ navigate<'ConnectFarcasterDCs'>({
+ name: 'ConnectFarcasterDCs',
+ params: {
+ userSelections: {
+ ...userSelections,
+ farcasterID: fid,
+ farcasterAvatarURL: farcasterAvatarURL,
+ },
+ },
+ });
+ return;
+ }
+
invariant(
!ethereumAccount || ethereumAccount.nonceTimestamp,
'nonceTimestamp must be set after connecting to Ethereum account',
@@ -87,6 +102,7 @@
...userSelections,
farcasterID: fid,
farcasterAvatarURL: farcasterAvatarURL,
+ farcasterDCsToken: null,
},
},
});
@@ -98,6 +114,7 @@
farcasterID: fid,
accountSelection: ethereumAccount,
farcasterAvatarURL: farcasterAvatarURL,
+ farcasterDCsToken: null,
};
setSkipEthereumLoginOnce(false);
navigate<'AvatarSelection'>({
@@ -116,14 +133,20 @@
);
const onSkip = React.useCallback(() => {
- if (cachedSelections.farcasterID || cachedSelections.farcasterAvatarURL) {
+ if (
+ cachedSelections.farcasterID ||
+ cachedSelections.farcasterAvatarURL ||
+ cachedSelections.farcasterDCsToken
+ ) {
setCachedSelections(
- ({ farcasterID, farcasterAvatarURL, ...rest }) => rest,
+ ({ farcasterID, farcasterAvatarURL, farcasterDCsToken, ...rest }) =>
+ rest,
);
}
goToNextStep();
}, [
cachedSelections.farcasterAvatarURL,
+ cachedSelections.farcasterDCsToken,
cachedSelections.farcasterID,
goToNextStep,
setCachedSelections,
diff --git a/native/account/registration/password-selection.react.js b/native/account/registration/password-selection.react.js
--- a/native/account/registration/password-selection.react.js
+++ b/native/account/registration/password-selection.react.js
@@ -27,7 +27,8 @@
+keyserverURL?: ?string,
+farcasterID: ?string,
+username: string,
- farcasterAvatarURL: ?string,
+ +farcasterAvatarURL: ?string,
+ +farcasterDCsToken: ?string,
},
};
diff --git a/native/account/registration/registration-terms.react.js b/native/account/registration/registration-terms.react.js
--- a/native/account/registration/registration-terms.react.js
+++ b/native/account/registration/registration-terms.react.js
@@ -32,6 +32,7 @@
+avatarData: ?AvatarData,
+siweBackupSecrets?: ?SignedMessage,
+farcasterAvatarURL: ?string,
+ +farcasterDCsToken: ?string,
},
};
@@ -63,8 +64,13 @@
const { navigation } = props;
const { reconnectEthereum } = navigation;
- const { coolOrNerdMode, keyserverURL, farcasterID, farcasterAvatarURL } =
- userSelections;
+ const {
+ coolOrNerdMode,
+ keyserverURL,
+ farcasterID,
+ farcasterAvatarURL,
+ farcasterDCsToken,
+ } = userSelections;
const navigateToConnectEthereum = React.useCallback(() => {
reconnectEthereum({
userSelections: {
@@ -72,6 +78,7 @@
keyserverURL,
farcasterID,
farcasterAvatarURL,
+ farcasterDCsToken,
},
});
}, [
@@ -80,6 +87,7 @@
keyserverURL,
farcasterID,
farcasterAvatarURL,
+ farcasterDCsToken,
]);
const onNonceExpired = React.useCallback(() => {
setCachedSelections(oldUserSelections => ({
diff --git a/native/account/registration/registration-types.js b/native/account/registration/registration-types.js
--- a/native/account/registration/registration-types.js
+++ b/native/account/registration/registration-types.js
@@ -48,6 +48,7 @@
+clearCachedSelections: () => void,
+onNonceExpired: () => mixed,
+onAlertAcknowledged?: () => mixed,
+ +farcasterDCsToken: ?string,
};
export type CachedUserSelections = {
@@ -60,6 +61,7 @@
+farcasterID?: string,
+siweBackupSecrets?: ?SignedMessage,
+farcasterAvatarURL?: ?string,
+ +farcasterDCsToken?: ?string,
};
export const ensAvatarSelection: AvatarData = {
diff --git a/native/account/registration/siwe-backup-message-creation.react.js b/native/account/registration/siwe-backup-message-creation.react.js
--- a/native/account/registration/siwe-backup-message-creation.react.js
+++ b/native/account/registration/siwe-backup-message-creation.react.js
@@ -141,6 +141,7 @@
+accountSelection: AccountSelection,
+avatarData: ?AvatarData,
+farcasterAvatarURL: ?string,
+ +farcasterDCsToken: ?string,
},
};
diff --git a/native/account/registration/username-selection.react.js b/native/account/registration/username-selection.react.js
--- a/native/account/registration/username-selection.react.js
+++ b/native/account/registration/username-selection.react.js
@@ -28,6 +28,7 @@
+keyserverURL?: ?string,
+farcasterID: ?string,
+farcasterAvatarURL: ?string,
+ +farcasterDCsToken: ?string,
},
};
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
@@ -1,58 +1,67 @@
// @flow
import * as React from 'react';
-import { View, Text } from 'react-native';
+import { Text, View } from 'react-native';
import { useStyles } from '../themes/colors.js';
import FarcasterLogo from '../vectors/farcaster-logo.react.js';
-type TextType = 'connect' | 'disconnect';
+type TextType = 'connect' | 'disconnect' | 'connect_DC';
type Props = {
+textType: TextType,
};
+const prompts = {
+ connect: {
+ headerText: 'Do you want to connect your Farcaster account?',
+ bodyText:
+ 'Connecting your Farcaster account lets us bootstrap your social ' +
+ 'graph. We’ll also surface communities based on your Farcaster ' +
+ 'channels.',
+ displayLogo: true,
+ },
+ disconnect: {
+ headerText: 'Disconnect from Farcaster',
+ bodyText: 'You can disconnect your Farcaster account at any time.',
+ displayLogo: true,
+ },
+ connect_DC: {
+ headerText: 'Do you want to connect your Farcaster Direct Casts?',
+ bodyText:
+ 'Connecting your Farcaster Direct Casts gives Comm read and ' +
+ 'write access to your Direct Cast messages. This allows you to send ' +
+ 'and receive Direct Cast messages using Comm.',
+ displayLogo: false,
+ },
+};
+
function FarcasterPrompt(props: Props): React.Node {
const { textType } = props;
- let headerText;
- if (textType === 'disconnect') {
- headerText = 'Disconnect from Farcaster';
- } else {
- headerText = 'Do you want to connect your Farcaster account?';
- }
+ const { headerText, bodyText, displayLogo } = prompts[textType];
- let bodyText;
- 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);
+
+ let farcasterLogo = null;
+ if (displayLogo) {
+ farcasterLogo = (
+ <View style={styles.farcasterLogoContainer}>
+ <FarcasterLogo />
+ </View>
+ );
}
- const styles = useStyles(unboundStyles);
- const farcasterPrompt = React.useMemo(
+ return React.useMemo(
() => (
<>
<Text style={styles.header}>{headerText}</Text>
<Text style={styles.body}>{bodyText}</Text>
- <View style={styles.farcasterLogoContainer}>
- <FarcasterLogo />
- </View>
+ {farcasterLogo}
</>
),
- [
- bodyText,
- headerText,
- styles.body,
- styles.farcasterLogoContainer,
- styles.header,
- ],
+ [bodyText, farcasterLogo, headerText, styles.body, styles.header],
);
-
- return farcasterPrompt;
}
const unboundStyles = {
diff --git a/native/farcaster-auth/farcaster-auth-utils.js b/native/farcaster-auth/farcaster-auth-utils.js
new file mode 100644
--- /dev/null
+++ b/native/farcaster-auth/farcaster-auth-utils.js
@@ -0,0 +1,41 @@
+// @flow
+
+import * as React from 'react';
+
+import { useSignFarcasterAuthMessage } from 'lib/components/farcaster-auth-context.js';
+
+function useGetAuthToken(): (
+ fid: string,
+ walletMnemonic: string,
+) => Promise<string> {
+ const signAuthMessage = useSignFarcasterAuthMessage();
+
+ return React.useCallback(
+ async (fid: string, walletMnemonic: string) => {
+ const nonceResponse = await fetch(
+ 'https://client.farcaster.xyz/v2/get-dc-nonce',
+ );
+ const nonceData = await nonceResponse.json();
+ const nonce = nonceData.result.nonce;
+
+ const signResult = await signAuthMessage({
+ nonce,
+ fid,
+ walletMnemonic,
+ });
+
+ const params = new URLSearchParams({
+ message: signResult.message,
+ signature: signResult.signature,
+ });
+ const tokenResponse = await fetch(
+ `https://client.farcaster.xyz/v2/get-dc-auth-token?${params.toString()}`,
+ );
+ const tokenData = await tokenResponse.json();
+ return tokenData.result.token;
+ },
+ [signAuthMessage],
+ );
+}
+
+export { useGetAuthToken };
diff --git a/native/navigation/route-names.js b/native/navigation/route-names.js
--- a/native/navigation/route-names.js
+++ b/native/navigation/route-names.js
@@ -8,6 +8,7 @@
import type { ConnectSecondaryDeviceParams } from '../account/qr-auth/connect-secondary-device.react.js';
import type { AvatarSelectionParams } from '../account/registration/avatar-selection.react.js';
import type { ConnectEthereumParams } from '../account/registration/connect-ethereum.react.js';
+import type { ConnectFarcasterDCsParams } from '../account/registration/connect-farcaster-dc.react.js';
import type { ConnectFarcasterParams } from '../account/registration/connect-farcaster.react.js';
import type { EmojiAvatarSelectionParams } from '../account/registration/emoji-avatar-selection.react.js';
import type { ExistingEthereumAccountParams } from '../account/registration/existing-ethereum-account.react.js';
@@ -141,6 +142,7 @@
export const RestoreSIWEBackupRouteName = 'RestoreSIWEBackup';
export const ExistingEthereumAccountRouteName = 'ExistingEthereumAccount';
export const ConnectFarcasterRouteName = 'ConnectFarcaster';
+export const ConnectFarcasterDCsRouteName = 'ConnectFarcasterDCs';
export const UsernameSelectionRouteName = 'UsernameSelection';
export const CommunityCreationRouteName = 'CommunityCreation';
export const CommunityConfigurationRouteName = 'CommunityConfiguration';
@@ -325,6 +327,7 @@
+ConnectEthereum: ConnectEthereumParams,
+ExistingEthereumAccount: ExistingEthereumAccountParams,
+ConnectFarcaster: ConnectFarcasterParams,
+ +ConnectFarcasterDCs: ConnectFarcasterDCsParams,
+CreateSIWEBackupMessage: CreateSIWEBackupMessageParams,
+UsernameSelection: UsernameSelectionParams,
+PasswordSelection: PasswordSelectionParams,
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Thu, Jan 15, 5:44 AM (11 h, 53 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5936330
Default Alt Text
D14911.1768455876.diff (21 KB)
Attached To
Mode
D14911: [native] Connect Farcaster DCs during registration
Attached
Detach File
Event Timeline
Log In to Comment