"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import type { Chain } from "../../../../chains/types.js"; import type { ThirdwebClient } from "../../../../client/client.js"; import { webLocalStorage } from "../../../../utils/storage/webStorage.js"; import { isEcosystemWallet } from "../../../../wallets/ecosystem/is-ecosystem-wallet.js"; import { linkProfile, preAuthenticate, } from "../../../../wallets/in-app/web/lib/auth/index.js"; import type { Wallet } from "../../../../wallets/interfaces/wallet.js"; import { useCustomTheme } from "../../../core/design-system/CustomThemeProvider.js"; import { fontSize } from "../../../core/design-system/index.js"; import { setLastAuthProvider } from "../../../core/utils/storage.js"; import { Container, Line, ModalHeader } from "../../ui/components/basic.js"; import { Button } from "../../ui/components/buttons.js"; import { FadeIn } from "../../ui/components/FadeIn.js"; import { OTPInput } from "../../ui/components/OTPInput.js"; import { Spacer } from "../../ui/components/Spacer.js"; import { Spinner } from "../../ui/components/Spinner.js"; import { Text } from "../../ui/components/text.js"; import { StyledButton } from "../../ui/design-system/elements.js"; import type { InAppWalletLocale } from "./locale/types.js"; type VerificationStatus = | "verifying" | "invalid" | "linking_error" | "valid" | "idle" | "payment_required"; type AccountStatus = | { type: "sending" } | { type: "sent" } | { type: "error"; message: string | undefined }; type ScreenToShow = "base" | "enter-password-or-recovery-code"; /** * @internal */ export function OTPLoginUI(props: { userInfo: { email: string } | { phone: string }; wallet: Wallet; locale: InAppWalletLocale; done: () => void; goBack?: () => void; client: ThirdwebClient; chain: Chain | undefined; size: "compact" | "wide"; isLinking?: boolean; }) { const { wallet, done, goBack, userInfo } = props; const isWideModal = props.size === "wide"; const locale = props.locale; const [otpInput, setOtpInput] = useState(""); const [verifyStatus, setVerifyStatus] = useState("idle"); const [error, setError] = useState(); const [accountStatus, setAccountStatus] = useState({ type: "sending", }); const [countdown, setCountdown] = useState(0); const ecosystem = isEcosystemWallet(wallet) ? { id: wallet.id, partnerId: wallet.getConfig()?.partnerId, } : undefined; const [screen] = useState("base"); const sendEmailOrSms = useCallback(async () => { setOtpInput(""); setVerifyStatus("idle"); setAccountStatus({ type: "sending" }); try { if ("email" in userInfo) { await preAuthenticate({ client: props.client, ecosystem, email: userInfo.email, strategy: "email", }); setAccountStatus({ type: "sent" }); setCountdown(60); // Start 60-second countdown } else if ("phone" in userInfo) { await preAuthenticate({ client: props.client, ecosystem, phoneNumber: userInfo.phone, strategy: "phone", }); setAccountStatus({ type: "sent" }); setCountdown(60); // Start 60-second countdown } else { throw new Error("Invalid userInfo"); } } catch (e) { console.error(e); setVerifyStatus("idle"); setAccountStatus({ type: "error", message: e instanceof Error ? e.message : undefined, }); } }, [props.client, userInfo, ecosystem]); async function connect(otp: string) { if ("email" in userInfo) { await wallet.connect({ chain: props.chain, client: props.client, email: userInfo.email, strategy: "email", verificationCode: otp, }); await setLastAuthProvider("email", webLocalStorage); } else if ("phone" in userInfo) { await wallet.connect({ chain: props.chain, client: props.client, phoneNumber: userInfo.phone, strategy: "phone", verificationCode: otp, }); await setLastAuthProvider("phone", webLocalStorage); } else { throw new Error("Invalid userInfo"); } } async function link(otp: string) { if ("email" in userInfo) { await linkProfile({ client: props.client, ecosystem, email: userInfo.email, strategy: "email", verificationCode: otp, }); } else if ("phone" in userInfo) { await linkProfile({ client: props.client, ecosystem, phoneNumber: userInfo.phone, strategy: "phone", verificationCode: otp, }); } } const verify = async (otp: string) => { if (otp.length !== 6) { return; } setVerifyStatus("verifying"); try { // verifies otp for UI feedback if (props.isLinking) { await link(otp); } else { await connect(otp); } done(); setVerifyStatus("valid"); } catch (e) { // TODO: More robust error handling if ( e instanceof Error && e?.message?.includes("PAYMENT_METHOD_REQUIRED") ) { setVerifyStatus("payment_required"); } else if ( e instanceof Error && (e.message.toLowerCase().includes("link") || e.message.toLowerCase().includes("profile")) ) { setVerifyStatus("linking_error"); setError(e.message); } else { setVerifyStatus("invalid"); } console.error("Authentication Error", e); } }; // send email on mount const emailSentOnMount = useRef(false); useEffect(() => { if (!emailSentOnMount.current) { emailSentOnMount.current = true; sendEmailOrSms(); } }, [sendEmailOrSms]); // Handle countdown timer useEffect(() => { if (countdown <= 0) return; const timer = setInterval(() => { setCountdown((current) => { if (current <= 1) { clearInterval(timer); return 0; } return current - 1; }); }, 1000); return () => clearInterval(timer); }, [countdown]); if (screen === "base") { return (
{ e.preventDefault(); }} > {!isWideModal && } {locale.emailLoginScreen.enterCodeSendTo} {"email" in userInfo ? userInfo.email : userInfo.phone} { verify(otpInput); }} setValue={(value) => { setOtpInput(value); setVerifyStatus("idle"); // reset error }} value={otpInput} /> {verifyStatus === "invalid" && ( {locale.emailLoginScreen.invalidCode} )} {verifyStatus === "linking_error" && ( {error || "Failed to verify code"} )} {verifyStatus === "payment_required" && ( {locale.maxAccountsExceeded} )} {verifyStatus === "verifying" ? ( ) : ( )} {!isWideModal && } {accountStatus.type === "error" && ( {accountStatus.message || locale.emailLoginScreen.failedToSendCode} )} {accountStatus.type === "sending" && ( {locale.emailLoginScreen.sendingCode} )} {accountStatus.type !== "sending" && ( 0 ? "default" : "pointer", opacity: countdown > 0 ? 0.5 : 1, }} className="tw-resend-button" type="button" > {countdown > 0 ? `Resend code in ${countdown} seconds` : locale.emailLoginScreen.resendCode} )}
); } return null; } const LinkButton = /* @__PURE__ */ StyledButton((_) => { const theme = useCustomTheme(); return { all: "unset", "&:hover": { color: theme.colors.primaryText, }, color: theme.colors.accentText, cursor: "pointer", fontSize: fontSize.sm, fontWeight: 500, textAlign: "center", width: "100%", }; });