Page MenuHomePhorge

D14911.1768455876.diff
No OneTemporary

Size
21 KB
Referenced Files
None
Subscribers
None

D14911.1768455876.diff

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

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)

Event Timeline