diff --git a/components/Onboarding/init-xmtp-client.ts b/components/Onboarding/init-xmtp-client.ts index b3dd2b60e..78bdc4265 100644 --- a/components/Onboarding/init-xmtp-client.ts +++ b/components/Onboarding/init-xmtp-client.ts @@ -1,4 +1,3 @@ -import { Signer } from "ethers"; import { Alert } from "react-native"; // import { invalidateProfileSocialsQuery } from "../../data/helpers/profiles/profilesUpdate"; @@ -13,8 +12,13 @@ import { awaitableAlert } from "../../utils/alert"; import logger from "../../utils/logger"; import { logoutAccount, waitForLogoutTasksDone } from "../../utils/logout"; import { sentryTrackMessage } from "../../utils/sentry"; -import { createXmtpClientFromSigner } from "../../utils/xmtpRN/signIn"; +import { + createXmtpClientFromSigner, + createXmtpClientFromViemAccount, +} from "../../utils/xmtpRN/signIn"; import { getXmtpClient } from "../../utils/xmtpRN/sync"; +import { Signer } from "ethers"; +import { LocalAccount } from "viem/accounts"; export async function initXmtpClient(args: { signer: Signer; @@ -49,6 +53,39 @@ export async function initXmtpClient(args: { } } +export async function initXmtpClientFromViemAccount(args: { + account: LocalAccount; + address: string; + privyAccountId?: string; + isEphemeral?: boolean; + pkPath?: string; +}) { + const { account, address, ...restArgs } = args; + + if (!account || !address) { + throw new Error("No signer or address"); + } + + try { + await createXmtpClientFromViemAccount(account, async () => { + await awaitableAlert( + translate("current_installation_revoked"), + translate("current_installation_revoked_description") + ); + throw new Error("Current installation revoked"); + }); + + await connectWithAddress({ + address, + ...restArgs, + }); + } catch (e) { + await logoutAccount(address, false, true, () => {}); + logger.error(e); + throw e; + } +} + type IBaseArgs = { address: string; }; diff --git a/features/conversation/conversation.nav.tsx b/features/conversation/conversation.nav.tsx index ee9bbda63..345c9773c 100644 --- a/features/conversation/conversation.nav.tsx +++ b/features/conversation/conversation.nav.tsx @@ -24,7 +24,7 @@ export function ConversationNav() { options={{ title: "", headerBackTitle: "", - headerBackVisible: false, + // headerBackVisible: false, }} name="Conversation" component={ConversationScreen} diff --git a/features/embedded-wallets/turnkey-embedded-wallet.ts b/features/embedded-wallets/turnkey-embedded-wallet.ts index abc5599ed..3aa2a424f 100644 --- a/features/embedded-wallets/turnkey-embedded-wallet.ts +++ b/features/embedded-wallets/turnkey-embedded-wallet.ts @@ -1,4 +1,4 @@ -import { onPasskeyCreate } from "@/utils/passkeys/createPasskey"; +import { onPasskeyCreate } from "@/utils/passkeys/create-passkey"; import { IEmbeddedWallet } from "./embedded-wallet.interface"; import { Client } from "@xmtp/react-native-sdk"; import { LocalAccount } from "viem/accounts"; diff --git a/features/onboarding/passkey/passkeyAuthStore.tsx b/features/onboarding/passkey/passkeyAuthStore.tsx index 1e36ebb1e..60b26adf5 100644 --- a/features/onboarding/passkey/passkeyAuthStore.tsx +++ b/features/onboarding/passkey/passkeyAuthStore.tsx @@ -1,12 +1,24 @@ import { createContext, memo, useContext, useRef } from "react"; import { createStore, useStore } from "zustand"; +import { LocalAccount } from "viem/accounts"; +import { TurnkeyStoreInfo } from "@/utils/passkeys/passkeys.interfaces"; type IPasskeyAuthStoreProps = { - loading: boolean; + loading?: boolean; + error?: string; + statusString?: string; + account?: LocalAccount; + turnkeyInfo?: TurnkeyStoreInfo; + previousPasskeyName?: string; }; type IPasskeyAuthStoreState = IPasskeyAuthStoreProps & { setLoading: (loading: boolean) => void; + setError: (error: string | undefined) => void; + setStatusString: (statusString: string | undefined) => void; + setAccount: (account: LocalAccount | undefined) => void; + setTurnkeyInfo: (turnkeyInfo: TurnkeyStoreInfo | undefined) => void; + setPreviousPasskeyName: (previousPasskeyName: string | undefined) => void; reset: () => void; }; @@ -34,11 +46,23 @@ export const PasskeyAuthStoreProvider = memo( const createPasskeyAuthStore = (initProps: IPasskeyAuthStoreProps) => { const DEFAULT_PROPS: IPasskeyAuthStoreProps = { loading: false, + error: undefined, + statusString: undefined, + account: undefined, + turnkeyInfo: undefined, + previousPasskeyName: undefined, }; return createStore()((set) => ({ ...DEFAULT_PROPS, ...initProps, - setLoading: (loading) => set({ loading }), + setLoading: (loading) => + loading ? set({ loading, error: undefined }) : set({ loading: false }), + setError: (error) => set({ error, statusString: undefined }), + setStatusString: (statusString) => set({ statusString }), + setAccount: (account) => set({ account }), + setTurnkeyInfo: (turnkeyInfo) => set({ turnkeyInfo }), + setPreviousPasskeyName: (previousPasskeyName) => + set({ previousPasskeyName }), reset: () => set(DEFAULT_PROPS), })); }; diff --git a/i18n/translations/en.ts b/i18n/translations/en.ts index f3d4b7637..7456a840b 100644 --- a/i18n/translations/en.ts +++ b/i18n/translations/en.ts @@ -54,6 +54,7 @@ export const en = { }, passkey: { title: "Create Passkey", + add_account_title: "Add account by passkey", subtitle: "Create a passkey to connect to Converse", createButton: "Create Passkey", }, diff --git a/navigation/OnboardingNavigator.tsx b/navigation/OnboardingNavigator.tsx index b60a952b7..6fa68109a 100644 --- a/navigation/OnboardingNavigator.tsx +++ b/navigation/OnboardingNavigator.tsx @@ -5,7 +5,7 @@ import { useColorScheme } from "react-native"; import { authScreensSharedScreenOptions } from "../screens/Navigation/Navigation"; import { stackGroupScreenOptions } from "../screens/Navigation/navHelpers"; import { OnboardingConnectWalletScreen } from "../screens/Onboarding/OnboardingConnectWalletScreen"; -import { OnboardingEphemeraScreen } from "../screens/Onboarding/OnboardingEphemeraScreen"; +import { OnboardingEphemeralScreen } from "../screens/Onboarding/OnboardingEphemeralScreen"; import { OnboardingGetStartedScreen } from "../screens/Onboarding/OnboardingGetStartedScreen"; import { OnboardingNotificationsScreen } from "../screens/Onboarding/OnboardingNotificationsScreen"; import { OnboardingPrivateKeyScreen } from "../screens/Onboarding/OnboardingPrivateKeyScreen"; @@ -76,7 +76,7 @@ export const OnboardingNavigator = memo(function OnboardingNavigator() { /> diff --git a/screens/Navigation/Navigation.tsx b/screens/Navigation/Navigation.tsx index 7bc357cc2..dc5b6d55b 100644 --- a/screens/Navigation/Navigation.tsx +++ b/screens/Navigation/Navigation.tsx @@ -10,13 +10,13 @@ import { useRouter } from "../../navigation/useNavigation"; import Accounts from "../Accounts/Accounts"; import { IdleScreen } from "../IdleScreen"; import { NewAccountConnectWalletScreen } from "../NewAccount/NewAccountConnectWalletScreen"; -import { NewAccountEphemeraScreen } from "../NewAccount/NewAccountEphemeraScreen"; +import { NewAccountEphemeralScreen } from "../NewAccount/NewAccountEphemeralScreen"; import { NewAccountPrivateKeyScreen } from "../NewAccount/NewAccountPrivateKeyScreen"; import { NewAccountPrivyScreen } from "../NewAccount/NewAccountPrivyScreen"; import { NewAccountScreen } from "../NewAccount/NewAccountScreen"; import { NewAccountUserProfileScreen } from "../NewAccount/NewAccountUserProfileScreen"; import { OnboardingConnectWalletScreen } from "../Onboarding/OnboardingConnectWalletScreen"; -import { OnboardingEphemeraScreen } from "../Onboarding/OnboardingEphemeraScreen"; +import { OnboardingEphemeralScreen } from "../Onboarding/OnboardingEphemeralScreen"; import { OnboardingGetStartedScreen } from "../Onboarding/OnboardingGetStartedScreen"; import { OnboardingNotificationsScreen } from "../Onboarding/OnboardingNotificationsScreen"; import { OnboardingPrivateKeyScreen } from "../Onboarding/OnboardingPrivateKeyScreen"; @@ -47,6 +47,7 @@ import WebviewPreviewNav, { } from "./WebviewPreviewNav"; import { translate } from "@/i18n"; import { OnboardingPasskeyScreen } from "../Onboarding/OnboardingPasskeyScreen"; +import { NewAccountPasskeyScreen } from "../NewAccount/NewAccountPasskeyScreen"; export type NavigationParamList = { Idle: undefined; @@ -72,7 +73,7 @@ export type NavigationParamList = { NewAccountPrivy: undefined; NewAccountPrivateKey: undefined; NewAccountEphemera: undefined; - + NewAccountPasskey: undefined; // Main Accounts: undefined; Blocked: undefined; @@ -99,7 +100,7 @@ export type NavigationParamList = { export const authScreensSharedScreenOptions: NativeStackNavigationOptions = { headerTitle: "", headerBackTitle: translate("back"), - headerBackVisible: false, + // headerBackVisible: false, headerShadowVisible: false, }; @@ -237,7 +238,7 @@ export function SignedOutNavigation() { /> @@ -291,7 +292,11 @@ const NewAccountNavigator = memo(function NewAccountNavigator() { /> + diff --git a/screens/NewAccount/NewAccountEphemeraScreen.tsx b/screens/NewAccount/NewAccountEphemeralScreen.tsx similarity index 98% rename from screens/NewAccount/NewAccountEphemeraScreen.tsx rename to screens/NewAccount/NewAccountEphemeralScreen.tsx index c8b43308c..284a8942f 100644 --- a/screens/NewAccount/NewAccountEphemeraScreen.tsx +++ b/screens/NewAccount/NewAccountEphemeralScreen.tsx @@ -18,7 +18,7 @@ import { sentryTrackError } from "../../utils/sentry"; import { NavigationParamList } from "../Navigation/Navigation"; import { isMissingConverseProfile } from "../Onboarding/Onboarding.utils"; -export function NewAccountEphemeraScreen( +export function NewAccountEphemeralScreen( props: NativeStackScreenProps ) { const router = useRouter(); diff --git a/screens/NewAccount/NewAccountPasskeyScreen.tsx b/screens/NewAccount/NewAccountPasskeyScreen.tsx new file mode 100644 index 000000000..118aef1dd --- /dev/null +++ b/screens/NewAccount/NewAccountPasskeyScreen.tsx @@ -0,0 +1,324 @@ +import React, { memo, useCallback, useRef } from "react"; + +import { NewAccountScreenComp } from "@components/NewAccount/NewAccountScreenComp"; +import { NewAccountPictoTitleSubtitle } from "@components/NewAccount/NewAccountTitleSubtitlePicto"; +import { + PasskeyAuthStoreProvider, + usePasskeyAuthStoreContext, +} from "@features/onboarding/passkey/passkeyAuthStore"; +import { translate } from "@i18n"; +import { useRouter } from "../../navigation/useNavigation"; +import { isMissingConverseProfile } from "../Onboarding/Onboarding.utils"; +import { addWalletToPasskey } from "@/utils/passkeys/add-wallet-to-passkey"; +import { initXmtpClientFromViemAccount } from "@/components/Onboarding/init-xmtp-client"; +import { Button } from "@/design-system/Button/Button"; +import { TextField } from "@/design-system/TextField/TextField"; +import { Text } from "@/design-system/Text"; +import { onPasskeyCreate } from "@/utils/passkeys/create-passkey"; +import { loadAccountFromPasskey } from "@/utils/passkeys/load-client-from-passkey"; + +export const NewAccountPasskeyScreen = memo(function () { + return ( + + + + ); +}); + +const Content = memo(function Content() { + const router = useRouter(); + + const loading = usePasskeyAuthStoreContext((state) => state.loading); + + const error = usePasskeyAuthStoreContext((state) => state.error); + + const statusString = usePasskeyAuthStoreContext( + (state) => state.statusString + ); + + const account = usePasskeyAuthStoreContext((state) => state.account); + + const turnkeyInfo = usePasskeyAuthStoreContext((state) => state.turnkeyInfo); + + const previousPasskeyName = usePasskeyAuthStoreContext( + (state) => state.previousPasskeyName + ); + + const setLoading = usePasskeyAuthStoreContext((state) => state.setLoading); + + const setError = usePasskeyAuthStoreContext((state) => state.setError); + + const setStatusString = usePasskeyAuthStoreContext( + (state) => state.setStatusString + ); + + const setAccount = usePasskeyAuthStoreContext((state) => state.setAccount); + + const setTurnkeyInfo = usePasskeyAuthStoreContext( + (state) => state.setTurnkeyInfo + ); + + const setPreviousPasskeyName = usePasskeyAuthStoreContext( + (state) => state.setPreviousPasskeyName + ); + + const inputTextRef = useRef(""); + + const handleCreatePasskey = useCallback(async () => { + try { + setLoading(true); + const account = await onPasskeyCreate({ + passkeyName: inputTextRef.current ?? "", + setStatusString, + setPreviousPasskeyName, + setTurnkeyInfo, + }); + if (!account) { + setError("No account created from Passkey"); + return; + } + setAccount(account); + } catch (e) { + setError(e instanceof Error ? e.message : "Unknown error"); + } finally { + setLoading(false); + } + }, [ + setAccount, + setError, + setLoading, + setPreviousPasskeyName, + setStatusString, + setTurnkeyInfo, + ]); + + const handleLoginWithPasskey = useCallback(async () => { + try { + setLoading(true); + const { account } = await loadAccountFromPasskey({ + setStatusString, + setPreviousPasskeyName, + setTurnkeyInfo, + }); + if (!account) { + setError("No account loaded from Passkey"); + return; + } + setAccount(account); + } catch (e) { + setError(e instanceof Error ? e.message : "Unknown error"); + } finally { + setLoading(false); + } + }, [ + setAccount, + setError, + setLoading, + setPreviousPasskeyName, + setStatusString, + setTurnkeyInfo, + ]); + + const handleAddWalletToPasskey = useCallback(async () => { + try { + setLoading(true); + if (!turnkeyInfo) { + setStatusString("No turnkey info, you must create a passkey first"); + return; + } + await addWalletToPasskey({ + subOrgId: turnkeyInfo.subOrganizationId, + setStatusString, + }); + setStatusString("Wallet added to passkey"); + } catch (e) { + setError( + e instanceof Error ? e.message : "Error adding wallet to passkey" + ); + } finally { + setLoading(false); + } + }, [turnkeyInfo, setError, setLoading, setStatusString]); + + const createXmtpClientFromAccount = useCallback(async () => { + try { + setLoading(true); + if (!account) { + setStatusString("Need to set an account first"); + return; + } + await initXmtpClientFromViemAccount({ + account, + address: account.address, + }); + setStatusString("Xmtp client created"); + if (isMissingConverseProfile()) { + router.navigate("NewAccountUserProfile"); + } else { + router.navigate("Chats"); + } + } catch (err) { + console.log("error creating Xmtp client", err); + setStatusString(""); + setError( + "Error creating Xmtp client : " + + (err instanceof Error ? err.message : "") + + (typeof err === "string" ? err : "") + ); + } finally { + setLoading(false); + } + }, [account, router, setError, setLoading, setStatusString]); + + const onboardWithPasskey = useCallback(async () => { + try { + setLoading(true); + const account = await onPasskeyCreate({ + passkeyName: inputTextRef.current ?? "", + setStatusString, + setPreviousPasskeyName, + setTurnkeyInfo, + }); + await initXmtpClientFromViemAccount({ + account, + address: account.address, + }); + if (isMissingConverseProfile()) { + router.navigate("NewAccountUserProfile"); + } else { + router.navigate("Chats"); + } + } catch (e) { + setError(e instanceof Error ? e.message : "Unknown error"); + } finally { + setLoading(false); + } + }, [ + setLoading, + setStatusString, + setPreviousPasskeyName, + setTurnkeyInfo, + router, + setError, + ]); + + const addWalletToExistingPasskey = useCallback(async () => { + try { + const { turnkeyInfo } = await loadAccountFromPasskey({ + setStatusString, + setPreviousPasskeyName, + setTurnkeyInfo, + }); + setStatusString("Got turnkey info"); + + if (!turnkeyInfo) { + throw new Error("No turnkey info, you must create a passkey first"); + } + + const { account } = await addWalletToPasskey({ + subOrgId: turnkeyInfo.subOrganizationId, + setStatusString, + }); + setStatusString("Got address and account" + account.address); + + await initXmtpClientFromViemAccount({ + account, + address: account.address, + }); + if (isMissingConverseProfile()) { + router.navigate("NewAccountUserProfile"); + } else { + router.navigate("Chats"); + } + } catch (e) { + setError(e instanceof Error ? e.message : "Unknown error"); + } finally { + setLoading(false); + } + }, [ + setStatusString, + setPreviousPasskeyName, + setTurnkeyInfo, + router, + setError, + setLoading, + ]); + + return ( + + + + {translate("passkey.add_account_title")} + + + {statusString && ( + + {statusString} + + )} + {previousPasskeyName && ( + + Previous passkey name: + {previousPasskeyName} + + )} + {turnkeyInfo && ( + + Turnkey info: + {JSON.stringify(turnkeyInfo)} + + )} + {error && ( + + {error} + + )} + {account && ( + + Account created: + {account.address} + + )} + { + inputTextRef.current = text; + }} + /> +