import { useMutation } from "@tanstack/react-query"; import { useCallback, useEffect, useRef, useState } from "react"; import { Alert, StyleSheet, TouchableOpacity, View } from "react-native"; import type { Chain } from "../../../../chains/types.js"; import type { ThirdwebClient } from "../../../../client/client.js"; import { nativeLocalStorage } from "../../../../utils/storage/nativeStorage.js"; import type { MultiStepAuthProviderType, PreAuthArgsType, } from "../../../../wallets/in-app/core/authentication/types.js"; import type { InAppWalletAuth, InAppWalletSocialAuth, } from "../../../../wallets/in-app/core/wallet/types.js"; import { preAuthenticate } from "../../../../wallets/in-app/native/auth/index.js"; import { hasStoredPasskey } from "../../../../wallets/in-app/native/auth/passkeys.js"; import type { Wallet } from "../../../../wallets/interfaces/wallet.js"; import { type SocialAuthOption, socialAuthOptions, } from "../../../../wallets/types.js"; import type { Theme } from "../../../core/design-system/index.js"; import { setLastAuthProvider } from "../../../core/utils/storage.js"; import { radius, spacing } from "../../design-system/index.js"; import { ThemedButton, ThemedButtonWithIcon } from "../components/button.js"; import { ThemedInput, ThemedInputWithSubmit } from "../components/input.js"; import { RNImage } from "../components/RNImage.js"; import { Spacer } from "../components/spacer.js"; import { ThemedText } from "../components/text.js"; import { getAuthProviderImage } from "../components/WalletImage.js"; import { APPLE_ICON, COINBASE_ICON, DISCORD_ICON, EPIC_ICON, FACEBOOK_ICON, FARCASTER_ICON, GITHUB_ICON, GOOGLE_ICON, LINE_ICON, STEAM_ICON, TELEGRAM_ICON, TIKTOK_ICON, TWITCH_ICON, X_ICON, } from "../icons/svgs.js"; import type { ModalState } from "./ConnectModal.js"; import { LoadingView } from "./LoadingView.js"; const defaultAuthOptions: InAppWalletAuth[] = [ "email", "phone", "passkey", "google", "facebook", "apple", ]; const socialIcons = { apple: APPLE_ICON, coinbase: COINBASE_ICON, discord: DISCORD_ICON, facebook: FACEBOOK_ICON, farcaster: FARCASTER_ICON, github: GITHUB_ICON, google: GOOGLE_ICON, line: LINE_ICON, steam: STEAM_ICON, telegram: TELEGRAM_ICON, twitch: TWITCH_ICON, x: X_ICON, tiktok: TIKTOK_ICON, epic: EPIC_ICON, }; type InAppWalletFormUIProps = { client: ThirdwebClient; theme: Theme; wallet: Wallet<"inApp">; setScreen: (screen: ModalState) => void; connector: (args: { wallet: Wallet; connectFn: (chain?: Chain) => Promise; authMethod?: InAppWalletAuth; }) => Promise; }; export function InAppWalletUI(props: InAppWalletFormUIProps) { const { wallet, theme } = props; const config = wallet.getConfig(); const authOptions = config?.auth?.options || defaultAuthOptions; const socialLogins = authOptions.filter((x) => socialAuthOptions.includes(x as SocialAuthOption), ) as InAppWalletSocialAuth[]; const [inputMode, setInputMode] = useState<"email" | "phone">("email"); return ( {socialLogins.map((auth) => ( ))} {authOptions.includes("email") ? ( inputMode === "email" ? ( ) : ( setInputMode("email")} theme={theme} title="Email address" /> ) ) : null} {authOptions.includes("phone") ? ( inputMode === "phone" ? ( ) : ( setInputMode("phone")} theme={theme} title="Phone number" /> ) ) : null} {authOptions.includes("passkey") ? ( { props.setScreen({ screen: "passkey", wallet }); }} theme={theme} title="Passkey" /> ) : null} {authOptions.includes("guest") ? : null} ); } function GuestLogin(props: InAppWalletFormUIProps) { const { theme, wallet, client, connector } = props; const connectInAppWallet = useCallback(() => { connector({ authMethod: "guest", connectFn: async () => { await wallet.connect({ client, strategy: "guest", }); await setLastAuthProvider("guest", nativeLocalStorage); return wallet; }, wallet, }); }, [connector, wallet, client]); return ( { connectInAppWallet(); }} theme={theme} title="Continue as guest" /> ); } function SocialLogin( props: InAppWalletFormUIProps & { auth: InAppWalletSocialAuth }, ) { const { theme, wallet, auth, client, connector } = props; // TODO (rn) - move this onPress and state up const strategy = props.auth; const connectInAppWallet = useCallback(() => { connector({ authMethod: auth, connectFn: async (chain) => { await wallet.connect({ chain, client, strategy: auth, }); await setLastAuthProvider(auth, nativeLocalStorage); return wallet; }, wallet, }); }, [connector, auth, wallet, client]); return ( ); } function PreOtpLogin( props: InAppWalletFormUIProps & { auth: PreAuthArgsType["strategy"]; }, ) { const { theme, auth, client, setScreen, wallet } = props; const [phoneOrEmail, setPhoneNumberOrEmail] = useState(""); const sendCode = useMutation({ mutationFn: async (options: { auth: PreAuthArgsType["strategy"]; phoneOrEmail: string; }) => { const { auth, phoneOrEmail } = options; if (auth === "phone") { if (phoneOrEmail.slice(0, 1) !== "+") { throw new Error( "Invalid phone number. Please include a country code.", ); } await preAuthenticate({ client, phoneNumber: phoneOrEmail, strategy: auth, }); } else { await preAuthenticate({ client, email: phoneOrEmail, strategy: auth, }); } }, }); return ( sendCode.mutate( { auth, phoneOrEmail, }, { onError: (error) => { // TODO (rn) - handle error toast or input error border/label Alert.alert("Error", error.message); }, onSuccess: (_, vars) => { if (vars.auth === "phone") { setScreen({ auth: { phoneNumber: vars.phoneOrEmail, strategy: vars.auth, }, screen: "otp", wallet, }); } else { setScreen({ auth: { email: vars.phoneOrEmail, strategy: vars.auth }, screen: "otp", wallet, }); } }, }, ) } placeholder={auth === "phone" ? "Phone number" : "Email address"} theme={theme} value={phoneOrEmail} /> ); } export function OtpLogin( props: InAppWalletFormUIProps & { auth: MultiStepAuthProviderType; }, ) { const { theme, auth, wallet, client, connector } = props; const [verificationCode, setVerificationCode] = useState(""); const connectInAppWallet = async () => { if (!verificationCode || !verificationCode) return; await connector({ authMethod: auth.strategy, connectFn: async (chain) => { if (auth.strategy === "phone") { await wallet.connect({ chain, client, phoneNumber: auth.phoneNumber, strategy: auth.strategy, verificationCode, }); } else { await wallet.connect({ chain, client, email: auth.email, strategy: auth.strategy, verificationCode, }); } await setLastAuthProvider(auth.strategy, nativeLocalStorage); return wallet; }, wallet, }); }; return ( <> Enter the verification code sent to {auth.strategy === "phone" ? auth.phoneNumber : auth.email} Verify ); } export function PasskeyView(props: InAppWalletFormUIProps) { const { theme, wallet, client } = props; const [screen, setScreen] = useState< "select" | "login" | "loading" | "signup" >("loading"); const triggered = useRef(false); useEffect(() => { if (triggered.current) { return; } triggered.current = true; hasStoredPasskey(client) .then((isStored) => { if (isStored) { setScreen("login"); } else { setScreen("select"); } }) .catch(() => { setScreen("select"); }); }, [client]); if (screen === "loading") { return ; } if (screen === "login" || screen === "signup") { return ( ); } return ( wallet && ( { setScreen("signup"); }} style={{ width: "100%" }} theme={theme} variant="accent" > Create a Passkey { setScreen("login"); }} style={{ width: "100%" }} theme={theme} variant="secondary" > I have a Passkey ) ); } function PasskeyLoadingView( props: InAppWalletFormUIProps & { type: "sign-in" | "sign-up"; }, ) { const { theme, type, wallet, client, connector } = props; const triggered = useRef(false); useEffect(() => { if (triggered.current) { return; } triggered.current = true; const connectInAppWallet = async (type: "sign-in" | "sign-up") => { await connector({ authMethod: "passkey", connectFn: async (chain) => { await wallet.connect({ chain, client, strategy: "passkey", type, }); await setLastAuthProvider("passkey", nativeLocalStorage); return wallet; }, wallet, }); }; connectInAppWallet(type); }, [client, type, wallet, connector]); return ; } const styles = StyleSheet.create({ container: { flexDirection: "column", gap: spacing.md, }, row: { flexDirection: "row", flexWrap: "wrap", gap: spacing.md, justifyContent: "space-evenly", }, socialIconContainer: { alignItems: "center", borderRadius: radius.lg, borderStyle: "solid", borderWidth: 1, flex: 1, flexDirection: "column", height: 60, justifyContent: "center", }, });