diff --git a/.changeset/afraid-peaches-admire.md b/.changeset/afraid-peaches-admire.md new file mode 100644 index 00000000000..1f554e714cb --- /dev/null +++ b/.changeset/afraid-peaches-admire.md @@ -0,0 +1,14 @@ +--- +"thirdweb": minor +--- + +Adds useProfiles hook to fetch linked profiles for the current wallet. + +```jsx + import { useProfiles } from "thirdweb/react"; + + const { data: profiles } = useProfiles(); + + console.log("Type:", profiles[0].type); // "discord" + console.log("Email:", profiles[0].email); // "john.doe@example.com" +``` diff --git a/.changeset/chilled-ants-float.md b/.changeset/chilled-ants-float.md new file mode 100644 index 00000000000..2cc6bacdcf5 --- /dev/null +++ b/.changeset/chilled-ants-float.md @@ -0,0 +1,19 @@ +--- +"thirdweb": minor +--- + +Adds SIWE authentication on in-app wallets + +```ts +import { inAppWallet } from "thirdweb/wallets" + +const wallet = inAppWallet(); +const account = await wallet.connect({ + client, + walletId: "io.metamask", + chainId: 1 // can be anything unless using smart accounts +}); +``` + +This will give you a new in-app wallet, **not** the injected provider wallet. + diff --git a/.changeset/wild-readers-relate.md b/.changeset/wild-readers-relate.md new file mode 100644 index 00000000000..8699f313e6a --- /dev/null +++ b/.changeset/wild-readers-relate.md @@ -0,0 +1,5 @@ +--- +"thirdweb": minor +--- + +Adds account linking to the Connect UI diff --git a/packages/thirdweb/src/exports/react.ts b/packages/thirdweb/src/exports/react.ts index 1d02c8d6cc5..5797378a17b 100644 --- a/packages/thirdweb/src/exports/react.ts +++ b/packages/thirdweb/src/exports/react.ts @@ -55,6 +55,7 @@ export { useSendCalls } from "../react/core/hooks/wallets/useSendCalls.js"; export { useSwitchActiveWalletChain } from "../react/core/hooks/wallets/useSwitchActiveWalletChain.js"; export { useCallsStatus } from "../react/core/hooks/wallets/useCallsStatus.js"; export { useWalletBalance } from "../react/core/hooks/others/useWalletBalance.js"; +export { useProfiles } from "../react/core/hooks/others/useProfiles.js"; // chain hooks export { useChainMetadata } from "../react/core/hooks/others/useChainQuery.js"; diff --git a/packages/thirdweb/src/exports/wallets/in-app.ts b/packages/thirdweb/src/exports/wallets/in-app.ts index af414ad3b9d..ee438595551 100644 --- a/packages/thirdweb/src/exports/wallets/in-app.ts +++ b/packages/thirdweb/src/exports/wallets/in-app.ts @@ -19,4 +19,4 @@ export type { export { hasStoredPasskey } from "../../wallets/in-app/web/lib/auth/passkeys.js"; -export { socialIcons } from "../../react/core/utils/socialIcons.js"; +export { socialIcons } from "../../react/core/utils/walletIcon.js"; diff --git a/packages/thirdweb/src/react/core/hooks/others/useProfiles.ts b/packages/thirdweb/src/react/core/hooks/others/useProfiles.ts new file mode 100644 index 00000000000..f42801d523c --- /dev/null +++ b/packages/thirdweb/src/react/core/hooks/others/useProfiles.ts @@ -0,0 +1,36 @@ +import { type UseQueryResult, useQuery } from "@tanstack/react-query"; +import type { Profile } from "../../../../wallets/in-app/core/authentication/types.js"; +import { getProfiles } from "../../../../wallets/in-app/core/wallet/profiles.js"; +import type { Wallet } from "../../../../wallets/interfaces/wallet.js"; +import { useActiveWallet } from "../wallets/useActiveWallet.js"; + +/** + * @description Retrieves all linked profiles for the current wallet. + * + * @returns A React Query result containing the linked profiles for the connected in-app wallet. + * + * @note This hook will only run if the connected wallet supports multi-auth (in-app wallets). + * + * @example + * ```jsx + * import { use } from "thirdweb/react"; + * + * const { data: profiles } = useProfiles(); + * + * console.log("Type:", profiles[0].type); // "discord" + * console.log("Email:", profiles[0].email); // "john.doe@example.com" + * ``` + * + * @wallet + */ +export function useProfiles(): UseQueryResult { + const wallet = useActiveWallet(); + + return useQuery({ + queryKey: ["profiles", wallet?.id], + enabled: !!wallet && wallet.id === "inApp", + queryFn: async () => { + return getProfiles(wallet as Wallet<"inApp">); + }, + }); +} diff --git a/packages/thirdweb/src/react/core/utils/socialIcons.ts b/packages/thirdweb/src/react/core/utils/walletIcon.ts similarity index 98% rename from packages/thirdweb/src/react/core/utils/socialIcons.ts rename to packages/thirdweb/src/react/core/utils/walletIcon.ts index 5465d902200..988fc993438 100644 --- a/packages/thirdweb/src/react/core/utils/socialIcons.ts +++ b/packages/thirdweb/src/react/core/utils/walletIcon.ts @@ -55,3 +55,28 @@ export const socialIcons = { farcaster: farcasterIconUri, telegram: telegramIconUri, }; + +export function getWalletIcon(provider: string) { + switch (provider) { + case "google": + return googleIconUri; + case "apple": + return appleIconUri; + case "facebook": + return facebookIconUri; + case "phone": + return phoneIcon; + case "email": + return emailIcon; + case "passkey": + return passkeyIcon; + case "discord": + return discordIconUri; + case "farcaster": + return farcasterIconUri; + case "telegram": + return telegramIconUri; + default: + return genericWalletIcon; + } +} diff --git a/packages/thirdweb/src/react/native/ui/components/TokenIcon.tsx b/packages/thirdweb/src/react/native/ui/components/TokenIcon.tsx index ff10fd91397..eca88bb3990 100644 --- a/packages/thirdweb/src/react/native/ui/components/TokenIcon.tsx +++ b/packages/thirdweb/src/react/native/ui/components/TokenIcon.tsx @@ -3,7 +3,7 @@ import type { Chain } from "../../../../chains/types.js"; import type { ThirdwebClient } from "../../../../client/client.js"; import type { Theme } from "../../../core/design-system/index.js"; import type { TokenInfo } from "../../../core/utils/defaultTokens.js"; -import { genericTokenIcon } from "../../../core/utils/socialIcons.js"; +import { genericTokenIcon } from "../../../core/utils/walletIcon.js"; import { ChainIcon } from "./ChainIcon.js"; import { RNImage } from "./RNImage.js"; diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx index fc559046a7c..0aacadda56f 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx @@ -53,6 +53,7 @@ import type { } from "../../../core/utils/defaultTokens.js"; import { hasSmartAccount } from "../../../core/utils/isSmartWallet.js"; import { useConnectedWalletDetails } from "../../../core/utils/wallet.js"; +import { WalletUIStatesProvider } from "../../providers/wallet-ui-states-provider.js"; import { ChainIcon } from "../components/ChainIcon.js"; import { CopyIcon } from "../components/CopyIcon.js"; import { Img } from "../components/Img.js"; @@ -68,6 +69,7 @@ import { fadeInAnimation } from "../design-system/animations.js"; import { StyledButton } from "../design-system/elements.js"; import type { LocaleId } from "../types.js"; import { MenuButton, MenuLink } from "./MenuButton.js"; +import { ScreenSetupContext, useSetupScreen } from "./Modal/screen.js"; import { NetworkSelectorContent, type NetworkSelectorProps, @@ -84,6 +86,8 @@ import { getConnectLocale } from "./locale/getConnectLocale.js"; import type { ConnectLocale } from "./locale/types.js"; import { LazyBuyScreen } from "./screens/Buy/LazyBuyScreen.js"; import { WalletManagerScreen } from "./screens/Details/WalletManagerScreen.js"; +import { LinkProfileScreen } from "./screens/LinkProfileScreen.js"; +import { LinkedProfilesScreen } from "./screens/LinkedProfilesScreen.js"; import { ManageWalletScreen } from "./screens/ManageWalletScreen.js"; import { PrivateKey } from "./screens/PrivateKey.js"; import { ReceiveFunds } from "./screens/ReceiveFunds.js"; @@ -277,6 +281,12 @@ function DetailsModal(props: { const disableSwitchChain = !activeWallet?.switchChain; + const screenSetup = useSetupScreen({ + size: "compact", + welcomeScreen: undefined, + wallets: activeWallet ? [activeWallet] : [], + }); + function closeModal() { setIsOpen(false); onModalUnmount(() => { @@ -778,6 +788,25 @@ function DetailsModal(props: { client={client} /> ); + } else if (screen === "linked-profiles") { + content = ( + setScreen("manage-wallet")} + client={client} + locale={locale} + setScreen={setScreen} + /> + ); + } else if (screen === "link-profile") { + content = ( + { + setScreen("linked-profiles"); + }} + client={client} + locale={locale} + /> + ); } // send funds @@ -832,17 +861,21 @@ function DetailsModal(props: { return ( - { - if (!_open) { - closeModal(); - } - }} - > - {content} - + + + { + if (!_open) { + closeModal(); + } + }} + > + {content} + + + ); } @@ -1191,6 +1224,7 @@ export type UseWalletDetailsModalOptions = { * ``` */ chains?: Chain[]; + /** * Show a "Request Testnet funds" link in Wallet Details Modal when user is connected to a testnet. * diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/Modal/AnyWalletConnectUI.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/Modal/AnyWalletConnectUI.tsx index ad023031ddf..1b28a1d5192 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/Modal/AnyWalletConnectUI.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/Modal/AnyWalletConnectUI.tsx @@ -16,7 +16,6 @@ import type { Wallet } from "../../../../../wallets/interfaces/wallet.js"; import type { EcosystemWalletId } from "../../../../../wallets/wallet-types.js"; import { iconSize } from "../../../../core/design-system/index.js"; import { useWalletInfo } from "../../../../core/utils/wallet.js"; -import EcosystemWalletConnectUI from "../../../wallets/ecosystem/EcosystemWalletConnectUI.js"; import { getInjectedWalletLocale } from "../../../wallets/injected/locale/getInjectedWalletLocale.js"; import { GetStartedScreen } from "../../../wallets/shared/GetStartedScreen.js"; import { LoadingScreen } from "../../../wallets/shared/LoadingScreen.js"; @@ -38,6 +37,9 @@ const CoinbaseSDKWalletConnectUI = /* @__PURE__ */ lazy( const InAppWalletConnectUI = /* @__PURE__ */ lazy( () => import("../../../wallets/in-app/InAppWalletConnectUI.js"), ); +const EcosystemWalletConnectUI = /* @__PURE__ */ lazy( + () => import("../../../wallets/ecosystem/EcosystemWalletConnectUI.js"), +); /** * @internal diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/Modal/screen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/Modal/screen.tsx index fff784c86f1..96d65521409 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/Modal/screen.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/Modal/screen.tsx @@ -70,7 +70,7 @@ export function useScreenContext() { const ctx = useContext(ScreenSetupContext); if (!ctx) { throw new Error( - "useScreenContext must be used within a ", + "useScreenContext must be used within a ", ); } return ctx; diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/WalletSelector.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/WalletSelector.tsx index ddc46a763f0..776fe4cdcfc 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/WalletSelector.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/WalletSelector.tsx @@ -16,7 +16,7 @@ import { radius, spacing, } from "../../../core/design-system/index.js"; -import { genericWalletIcon } from "../../../core/utils/socialIcons.js"; +import { genericWalletIcon } from "../../../core/utils/walletIcon.js"; import { useSetSelectionData } from "../../providers/wallet-ui-states-provider.js"; import { sortWallets } from "../../utils/sortWallets.js"; import { LoadingScreen } from "../../wallets/shared/LoadingScreen.js"; diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/icons/AddUserIcon.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/icons/AddUserIcon.tsx new file mode 100644 index 00000000000..205af6cdf6c --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/icons/AddUserIcon.tsx @@ -0,0 +1,25 @@ +import type { IconFC } from "./types.js"; + +/** + * @internal + */ +export const AddUserIcon: IconFC = (props) => { + return ( + + + + ); +}; diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/icons/MultiUserIcon.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/icons/MultiUserIcon.tsx new file mode 100644 index 00000000000..61f28a1dec1 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/icons/MultiUserIcon.tsx @@ -0,0 +1,25 @@ +import type { IconFC } from "./types.js"; + +/** + * @internal + */ +export const MultiUserIcon: IconFC = (props) => { + return ( + + + + ); +}; diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/de.ts b/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/de.ts index ccfe4bd474d..f15d03bd66a 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/de.ts +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/de.ts @@ -37,6 +37,10 @@ const connectLocaleDe: ConnectLocale = { smartWallet: "Smart Wallet", or: "Oder", goBackButton: "Zurück", + passkeys: { + title: "Passkeys", + linkPasskey: "Passkey verknüpfen", + }, welcomeScreen: { defaultTitle: "Dein Tor zur dezentralen Welt", defaultSubtitle: "Verbinde ein Wallet, um loszulegen", @@ -104,6 +108,8 @@ const connectLocaleDe: ConnectLocale = { manageWallet: { title: "Wallet verwalten", connectAnApp: "App verbinden", + linkProfile: "Profil verknüpfen", + linkedProfiles: "Verknüpfte Profile", exportPrivateKey: "PrivateKey exportieren", }, viewFunds: { diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/en.ts b/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/en.ts index a48b45761bb..9ce84a49be9 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/en.ts +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/en.ts @@ -37,6 +37,10 @@ const connectLocaleEn: ConnectLocale = { smartWallet: "Smart Wallet", or: "OR", goBackButton: "Back", + passkeys: { + title: "Passkeys", + linkPasskey: "Link a Passkey", + }, welcomeScreen: { defaultTitle: "Your gateway to the decentralized world", defaultSubtitle: "Connect a wallet to get started", @@ -102,6 +106,8 @@ const connectLocaleEn: ConnectLocale = { }, manageWallet: { title: "Manage Wallet", + linkedProfiles: "Linked Profiles", + linkProfile: "Link a Profile", connectAnApp: "Connect an App", exportPrivateKey: "Export Private Key", }, diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/es.ts b/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/es.ts index 104390077fd..50ff28bf54e 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/es.ts +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/es.ts @@ -37,6 +37,10 @@ const connectWalletLocalEs: ConnectLocale = { smartWallet: "Cartera inteligente", or: "O", goBackButton: "Atras", + passkeys: { + title: "Clave de acceso", + linkPasskey: "Vincular una clave de acceso", + }, welcomeScreen: { defaultTitle: "Tu puerta de entrada al mundo descentralizado", defaultSubtitle: "Conecta una cartera para empezar", @@ -104,6 +108,8 @@ const connectWalletLocalEs: ConnectLocale = { }, manageWallet: { title: "Gestionar Cartera", + linkedProfiles: "Perfiles vinculados", + linkProfile: "Vincular un perfil", connectAnApp: "Conectar una Aplicación", exportPrivateKey: "Exportar Clave Privada", }, diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/fr.ts b/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/fr.ts index f2d035d92dc..2e5581f3984 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/fr.ts +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/fr.ts @@ -37,6 +37,10 @@ const connectLocaleFr: ConnectLocale = { smartWallet: "Portefeuille intelligent", or: "OU", goBackButton: "Retour", + passkeys: { + title: "Clés d'accès", + linkPasskey: "Lier une clé d'accès", + }, welcomeScreen: { defaultTitle: "Votre porte d'entrée vers le monde décentralisé", defaultSubtitle: "Connectez un portefeuille pour commencer", @@ -104,6 +108,8 @@ const connectLocaleFr: ConnectLocale = { }, manageWallet: { title: "Gérer le portefeuille", + linkedProfiles: "Profils liés", + linkProfile: "Lier un profil", connectAnApp: "Connecter une application", exportPrivateKey: "Exporter la clé privée", }, diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/ja.ts b/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/ja.ts index c92a7250b4d..19a25356681 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/ja.ts +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/ja.ts @@ -37,6 +37,10 @@ const connectWalletLocalJa: ConnectLocale = { smartWallet: "スマートウォレット", or: "または", goBackButton: "戻る", // TODO - check translation + passkeys: { + linkPasskey: "パスキーをリンクする", + title: "パスキー", + }, welcomeScreen: { defaultTitle: "分散型世界へのゲートウェイ", defaultSubtitle: "始めるためにウォレットを接続してください", @@ -104,6 +108,8 @@ const connectWalletLocalJa: ConnectLocale = { }, manageWallet: { title: "ウォレットを管理", + linkedProfiles: "リンクされたプロファイル", + linkProfile: "プロフィールをリンクする", connectAnApp: "アプリを接続", exportPrivateKey: "秘密鍵をエクスポート", }, diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/kr.ts b/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/kr.ts index f68455a9394..6cffc7fc9c0 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/kr.ts +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/kr.ts @@ -36,6 +36,10 @@ const connectLocaleKr: ConnectLocale = { smartWallet: "스마트 지갑", or: "또는", goBackButton: "뒤로", + passkeys: { + title: "패스키", + linkPasskey: "패스키 연결", + }, welcomeScreen: { defaultTitle: "분산된 세계로의 게이트웨이", defaultSubtitle: "시작하려면 지갑을 연결하십시오", @@ -101,6 +105,8 @@ const connectLocaleKr: ConnectLocale = { }, manageWallet: { title: "지갑 관리", + linkedProfiles: "연결된 프로필", + linkProfile: "링크 프로필", connectAnApp: "앱 연결", exportPrivateKey: "개인 키 내보내기", }, diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/tl.ts b/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/tl.ts index 129fb61fef2..2177eddb309 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/tl.ts +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/tl.ts @@ -37,7 +37,10 @@ const connectWalletLocalTl: ConnectLocale = { smartWallet: "Smart Wallet", or: "O", goBackButton: "Bumalik", - + passkeys: { + title: "Mga Passkey", + linkPasskey: "I-link ang Passkey", + }, welcomeScreen: { defaultTitle: "Ang iyong daan patungo sa decentralized na mundo", defaultSubtitle: "Kumonekta ng wallet para magsimula", @@ -104,6 +107,8 @@ const connectWalletLocalTl: ConnectLocale = { }, manageWallet: { title: "Pamahalaan ang Wallet", + linkedProfiles: "Linked Profiles", + linkProfile: "Link a Profile", connectAnApp: "Ikonekta ang isang App", exportPrivateKey: "I-export ang Pribadong Susi", }, diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/types.ts b/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/types.ts index f61fb96f673..8a17d5da2ce 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/types.ts +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/types.ts @@ -25,6 +25,10 @@ export type ConnectLocale = { buy: string; guestWalletWarning: string; installed: string; + passkeys: { + title: string; + linkPasskey: string; + }; networkSelector: { addCustomNetwork: string; allNetworks: string; @@ -94,7 +98,9 @@ export type ConnectLocale = { welcomeScreen: { defaultSubtitle: string; defaultTitle: string }; manageWallet: { title: string; + linkedProfiles: string; connectAnApp: string; + linkProfile: string; exportPrivateKey: string; }; viewFunds: { diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/vi.ts b/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/vi.ts index 9373bdd5f3f..6b76795f549 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/vi.ts +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/locale/vi.ts @@ -37,6 +37,10 @@ const connectLocaleVi: ConnectLocale = { smartWallet: "Ví Thông Minh", or: "hoặc", goBackButton: "Quay lại", + passkeys: { + title: "Khóa truy cập", + linkPasskey: "Liên kết khóa truy cập", + }, welcomeScreen: { defaultTitle: "Cánh cổng dẫn tới thế giới phi tập trung", defaultSubtitle: "Kết nối ví để bắt đầu", @@ -102,6 +106,8 @@ const connectLocaleVi: ConnectLocale = { }, manageWallet: { title: "Quản lý ví", + linkedProfiles: "Tài khoản", + linkProfile: "Thêm tính năng xác thực", connectAnApp: "Kết nối ứng dụng", exportPrivateKey: "Sao lưu private key", }, diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/LinkProfileScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/LinkProfileScreen.tsx new file mode 100644 index 00000000000..2eac855fd5f --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/LinkProfileScreen.tsx @@ -0,0 +1,88 @@ +"use client"; +import { CrossCircledIcon } from "@radix-ui/react-icons"; +import { useQueryClient } from "@tanstack/react-query"; +import { Suspense, lazy } from "react"; +import type { ThirdwebClient } from "../../../../../client/client.js"; +import type { Wallet } from "../../../../../wallets/interfaces/wallet.js"; +import { iconSize } from "../../../../core/design-system/index.js"; +import { useActiveWallet } from "../../../../core/hooks/wallets/useActiveWallet.js"; +import { useActiveWalletChain } from "../../../../core/hooks/wallets/useActiveWalletChain.js"; +import { LoadingScreen } from "../../../wallets/shared/LoadingScreen.js"; +import { Container, Line, ModalHeader } from "../../components/basic.js"; +import { Text } from "../../components/text.js"; +import type { ConnectLocale } from "../locale/types.js"; + +const InAppWalletConnectUI = /* @__PURE__ */ lazy( + () => import("../../../wallets/in-app/InAppWalletConnectUI.js"), +); + +/** + * @internal + */ +export function LinkProfileScreen(props: { + onBack: () => void; + locale: ConnectLocale; + client: ThirdwebClient; +}) { + const activeWallet = useActiveWallet(); + const chain = useActiveWalletChain(); + const queryClient = useQueryClient(); + + if (!activeWallet) { + return ; + } + + if (activeWallet.id === "inApp") { + return ( + }> + } + done={() => { + queryClient.invalidateQueries({ queryKey: ["profiles"] }); + props.onBack(); + }} + connectLocale={props.locale} + client={props.client} + size="compact" + chain={chain} + meta={{ + title: props.locale.manageWallet.linkProfile, + showThirdwebBranding: false, + }} + isLinking={true} + goBack={props.onBack} + /> + + ); + } + + return ( + + + + + + + + This wallet does not support account linking + + + ); +} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.tsx new file mode 100644 index 00000000000..6c1faa5d7fe --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/LinkedProfilesScreen.tsx @@ -0,0 +1,119 @@ +"use client"; +import type { ThirdwebClient } from "../../../../../client/client.js"; +import { shortenAddress } from "../../../../../utils/address.js"; +import type { Profile } from "../../../../../wallets/in-app/core/authentication/types.js"; +import { fontSize, iconSize } from "../../../../core/design-system/index.js"; +import { useProfiles } from "../../../../core/hooks/others/useProfiles.js"; +import { getWalletIcon } from "../../../../core/utils/walletIcon.js"; +import { LoadingScreen } from "../../../wallets/shared/LoadingScreen.js"; +import { Img } from "../../components/Img.js"; +import { Spacer } from "../../components/Spacer.js"; +import { Container, Line, ModalHeader } from "../../components/basic.js"; +import { Text } from "../../components/text.js"; +import { MenuButton } from "../MenuButton.js"; +import { AddUserIcon } from "../icons/AddUserIcon.js"; +import type { ConnectLocale } from "../locale/types.js"; +import type { WalletDetailsModalScreen } from "./types.js"; + +function getProfileDisplayName(profile: Profile) { + switch (true) { + case profile.type === "email" && profile.details.email !== undefined: + return profile.details.email; + case profile.type === "google" && profile.details.email !== undefined: + return profile.details.email; + case profile.type === "phone" && profile.details.phone !== undefined: + return profile.details.phone; + case profile.details.address !== undefined: + return shortenAddress(profile.details.address, 6); + case (profile.type as string) === "cognito" && + profile.details.email !== undefined: + return profile.details.email; + default: + return profile.type.slice(0, 1).toUpperCase() + profile.type.slice(1); + } +} + +/** + * @internal + */ +export function LinkedProfilesScreen(props: { + onBack: () => void; + setScreen: (screen: WalletDetailsModalScreen) => void; + locale: ConnectLocale; + client: ThirdwebClient; +}) { + const { data: connectedProfiles, isLoading } = useProfiles(); + + if (isLoading) { + return ; + } + + return ( + + + + + + {isLoading ? ( + + ) : ( + + + + { + props.setScreen("link-profile"); + }} + style={{ + fontSize: fontSize.sm, + }} + > + + + {props.locale.manageWallet.linkProfile} + + + + {connectedProfiles?.map((profile) => ( + { + props.setScreen("linked-profiles"); + }} + style={{ + fontSize: fontSize.sm, + cursor: "default", + }} + disabled // disabled until we have more data to show on a dedicated profile screen + > + + + {getProfileDisplayName(profile)} + + + ))} + + + + )} + + ); +} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/ManageWalletScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/ManageWalletScreen.tsx index 94d25a66d66..c0708ff3356 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/ManageWalletScreen.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/ManageWalletScreen.tsx @@ -1,7 +1,6 @@ "use client"; import { ShuffleIcon } from "@radix-ui/react-icons"; import type { ThirdwebClient } from "../../../../../client/client.js"; -import { isEcosystemWallet } from "../../../../../wallets/ecosystem/is-ecosystem-wallet.js"; import { isInAppWallet } from "../../../../../wallets/in-app/core/wallet/index.js"; import { injectedProvider } from "../../../../../wallets/injected/mipdStore.js"; import { fontSize, iconSize } from "../../../../core/design-system/index.js"; @@ -11,6 +10,7 @@ import { Container, Line, ModalHeader } from "../../components/basic.js"; import { Text } from "../../components/text.js"; import { MenuButton } from "../MenuButton.js"; import { KeyIcon } from "../icons/KeyIcon.js"; +import { MultiUserIcon } from "../icons/MultiUserIcon.js"; import { WalletConnectIcon } from "../icons/WalletConnectIcon.js"; import type { ConnectLocale } from "../locale/types.js"; import type { WalletDetailsModalScreen } from "./types.js"; @@ -55,6 +55,23 @@ export function ManageWalletScreen(props: { connectLocale={props.locale} /> + {/* Multi-auth */} + {activeWallet?.id === "inApp" && ( + { + props.setScreen("linked-profiles"); + }} + style={{ + fontSize: fontSize.sm, + }} + > + + + {props.locale.manageWallet.linkedProfiles} + + + )} + {/* Wallet Connect Receiver */} { @@ -72,9 +89,8 @@ export function ManageWalletScreen(props: { {/* Private Key Export (if enabled) */} {activeWallet && - ((isInAppWallet(activeWallet) && - !activeWallet.getConfig()?.hidePrivateKeyExport) || - isEcosystemWallet(activeWallet)) && ( + isInAppWallet(activeWallet) && + !activeWallet.getConfig()?.hidePrivateKeyExport && ( { props.setScreen("private-key"); diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/types.ts b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/types.ts index d25e95d7470..32d12e2a839 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/types.ts +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/types.ts @@ -73,4 +73,6 @@ export type WalletDetailsModalScreen = | "private-key" | "manage-wallet" | "wallet-connect-receiver" - | "wallet-manager"; + | "wallet-manager" + | "link-profile" + | "linked-profiles"; diff --git a/packages/thirdweb/src/react/web/ui/components/Spinner.tsx b/packages/thirdweb/src/react/web/ui/components/Spinner.tsx index 7739cb8714e..89d42257d3b 100644 --- a/packages/thirdweb/src/react/web/ui/components/Spinner.tsx +++ b/packages/thirdweb/src/react/web/ui/components/Spinner.tsx @@ -27,7 +27,7 @@ export const Spinner: React.FC<{ r="20" fill="none" stroke={theme.colors[props.color]} - strokeWidth="4" + strokeWidth={Number(iconSize[props.size]) > 64 ? "2" : "4"} /> ); diff --git a/packages/thirdweb/src/react/web/ui/components/TokenIcon.tsx b/packages/thirdweb/src/react/web/ui/components/TokenIcon.tsx index 58f998c6ab8..0949a0efdbb 100644 --- a/packages/thirdweb/src/react/web/ui/components/TokenIcon.tsx +++ b/packages/thirdweb/src/react/web/ui/components/TokenIcon.tsx @@ -5,7 +5,7 @@ import type { ThirdwebClient } from "../../../../client/client.js"; import { NATIVE_TOKEN_ADDRESS } from "../../../../constants/addresses.js"; import { iconSize } from "../../../core/design-system/index.js"; import { useChainIconUrl } from "../../../core/hooks/others/useChainQuery.js"; -import { genericTokenIcon } from "../../../core/utils/socialIcons.js"; +import { genericTokenIcon } from "../../../core/utils/walletIcon.js"; import { type NativeToken, isNativeToken, diff --git a/packages/thirdweb/src/react/web/ui/components/WalletImage.tsx b/packages/thirdweb/src/react/web/ui/components/WalletImage.tsx index de8340613a8..dd79dae1106 100644 --- a/packages/thirdweb/src/react/web/ui/components/WalletImage.tsx +++ b/packages/thirdweb/src/react/web/ui/components/WalletImage.tsx @@ -8,20 +8,12 @@ import { getStoredActiveWalletId } from "../../../../wallets/manager/index.js"; import type { WalletId } from "../../../../wallets/wallet-types.js"; import { radius } from "../../../core/design-system/index.js"; import { useActiveWallet } from "../../../core/hooks/wallets/useActiveWallet.js"; -import { - appleIconUri, - discordIconUri, - emailIcon, - facebookIconUri, - farcasterIconUri, - genericWalletIcon, - googleIconUri, - passkeyIcon, - phoneIcon, - telegramIconUri, -} from "../../../core/utils/socialIcons.js"; import { getLastAuthProvider } from "../../../core/utils/storage.js"; import { useWalletImage } from "../../../core/utils/wallet.js"; +import { + genericWalletIcon, + getWalletIcon, +} from "../../../core/utils/walletIcon.js"; import { Img } from "./Img.js"; /** @@ -57,35 +49,9 @@ export function WalletImage(props: { ) { // when showing an active wallet icon - check last auth provider and override the IAW icon const lastAuthProvider = await getLastAuthProvider(storage); - switch (lastAuthProvider) { - case "google": - image = googleIconUri; - break; - case "apple": - image = appleIconUri; - break; - case "facebook": - image = facebookIconUri; - break; - case "phone": - image = phoneIcon; - break; - case "email": - image = emailIcon; - break; - case "passkey": - image = passkeyIcon; - break; - case "discord": - image = discordIconUri; - break; - case "farcaster": - image = farcasterIconUri; - break; - case "telegram": - image = telegramIconUri; - break; - } + image = lastAuthProvider + ? getWalletIcon(lastAuthProvider) + : genericWalletIcon; } else { const mipdImage = getInstalledWalletProviders().find( (x) => x.info.rdns === activeEOAId, diff --git a/packages/thirdweb/src/react/web/wallets/ecosystem/EcosystemWalletConnectUI.tsx b/packages/thirdweb/src/react/web/wallets/ecosystem/EcosystemWalletConnectUI.tsx index 8c3ee356c6b..e53b67d2d20 100644 --- a/packages/thirdweb/src/react/web/wallets/ecosystem/EcosystemWalletConnectUI.tsx +++ b/packages/thirdweb/src/react/web/wallets/ecosystem/EcosystemWalletConnectUI.tsx @@ -35,6 +35,7 @@ function EcosystemWalletConnectUI(props: { termsOfServiceUrl?: string; privacyPolicyUrl?: string; }; + isLinking?: boolean; }) { const data = useSelectionData(); const setSelectionData = useSetSelectionData(); @@ -53,6 +54,11 @@ function EcosystemWalletConnectUI(props: { setSelectionData({}); }; + const done = () => { + props.done(); + setSelectionData({}); + }; + const otpUserInfo = state?.emailLogin ? { email: state.emailLogin } : state?.phoneLogin @@ -64,12 +70,13 @@ function EcosystemWalletConnectUI(props: { ); } @@ -77,12 +84,14 @@ function EcosystemWalletConnectUI(props: { if (state?.passkeyLogin) { return ( ); } @@ -92,13 +101,15 @@ function EcosystemWalletConnectUI(props: { ); } @@ -107,7 +118,7 @@ function EcosystemWalletConnectUI(props: { {}} locale={locale} - done={props.done} + done={done} goBack={props.goBack} wallet={props.wallet} chain={props.chain} @@ -115,6 +126,7 @@ function EcosystemWalletConnectUI(props: { size={props.size} connectLocale={props.connectLocale} meta={props.meta} + isLinking={props.isLinking} /> ); } diff --git a/packages/thirdweb/src/react/web/wallets/ecosystem/EcosystemWalletFormUI.tsx b/packages/thirdweb/src/react/web/wallets/ecosystem/EcosystemWalletFormUI.tsx index 46ad991c422..8c354b551bc 100644 --- a/packages/thirdweb/src/react/web/wallets/ecosystem/EcosystemWalletFormUI.tsx +++ b/packages/thirdweb/src/react/web/wallets/ecosystem/EcosystemWalletFormUI.tsx @@ -8,7 +8,7 @@ import { useScreenContext } from "../../ui/ConnectWallet/Modal/screen.js"; import { PoweredByThirdweb } from "../../ui/ConnectWallet/PoweredByTW.js"; import type { ConnectLocale } from "../../ui/ConnectWallet/locale/types.js"; import { Spacer } from "../../ui/components/Spacer.js"; -import { Container } from "../../ui/components/basic.js"; +import { Container, ModalHeader } from "../../ui/components/basic.js"; import { ConnectWalletSocialOptions } from "../shared/ConnectWalletSocialOptions.js"; import type { InAppWalletLocale } from "../shared/locale/types.js"; import { EcosystemWalletHeader } from "./EcosystemWalletHeader.js"; @@ -30,6 +30,7 @@ export type EcosystemWalletFormUIProps = { client: ThirdwebClient; chain: Chain | undefined; connectLocale: ConnectLocale; + isLinking?: boolean; }; /** @@ -54,11 +55,18 @@ export function EcosystemWalletFormUIScreen(props: EcosystemWalletFormUIProps) { minHeight: "250px", }} > - + {props.isLinking ? ( + + ) : ( + + )} void; goBack?: () => void; size: "compact" | "wide"; - meta: { + meta?: { title?: string; titleIconUrl?: string; showThirdwebBranding?: boolean; @@ -35,6 +35,7 @@ function InAppWalletConnectUI(props: { client: ThirdwebClient; chain: Chain | undefined; connectLocale: ConnectLocale; + isLinking?: boolean; }) { const data = useSelectionData(); const setSelectionData = useSetSelectionData(); @@ -56,6 +57,11 @@ function InAppWalletConnectUI(props: { } : props.goBack; + const done = () => { + props.done(); + setSelectionData({}); + }; + const otpUserInfo = state?.emailLogin ? { email: state.emailLogin } : state?.phoneLogin @@ -67,12 +73,13 @@ function InAppWalletConnectUI(props: { ); } @@ -80,12 +87,14 @@ function InAppWalletConnectUI(props: { if (state?.passkeyLogin) { return ( ); } @@ -95,13 +104,15 @@ function InAppWalletConnectUI(props: { ); } @@ -111,13 +122,14 @@ function InAppWalletConnectUI(props: { select={() => {}} connectLocale={props.connectLocale} inAppWalletLocale={locale} - done={props.done} + done={done} goBack={props.goBack} wallet={props.wallet} client={props.client} meta={props.meta} size={props.size} chain={props.chain} + isLinking={props.isLinking} /> ); } diff --git a/packages/thirdweb/src/react/web/wallets/in-app/InAppWalletFormUI.tsx b/packages/thirdweb/src/react/web/wallets/in-app/InAppWalletFormUI.tsx index f64ab57bbeb..f75c8a79d8a 100644 --- a/packages/thirdweb/src/react/web/wallets/in-app/InAppWalletFormUI.tsx +++ b/packages/thirdweb/src/react/web/wallets/in-app/InAppWalletFormUI.tsx @@ -22,7 +22,7 @@ export type InAppWalletFormUIProps = { wallet: Wallet<"inApp">; goBack?: () => void; size: "compact" | "wide"; - meta: { + meta?: { title?: string; titleIconUrl?: string; showThirdwebBranding?: boolean; @@ -31,6 +31,7 @@ export type InAppWalletFormUIProps = { }; client: ThirdwebClient; chain: Chain | undefined; + isLinking?: boolean; }; /** @@ -43,7 +44,7 @@ export function InAppWalletFormUIScreen(props: InAppWalletFormUIProps) { const isInitialScreen = screen === props.wallet && initialScreen === props.wallet; - const onBack = isInitialScreen ? undefined : props.goBack; + const onBack = isInitialScreen && !props.isLinking ? undefined : props.goBack; return ( - {!props.meta.titleIconUrl ? null : ( + {!props.meta?.titleIconUrl ? null : ( )} - {props.meta.title ?? + {props.meta?.title ?? props.inAppWalletLocale.emailLoginScreen.title} @@ -97,18 +98,18 @@ export function InAppWalletFormUIScreen(props: InAppWalletFormUIProps) { {isCompact && - (props.meta.showThirdwebBranding !== false || - props.meta.termsOfServiceUrl || - props.meta.privacyPolicyUrl) && } + (props.meta?.showThirdwebBranding !== false || + props.meta?.termsOfServiceUrl || + props.meta?.privacyPolicyUrl) && } - {props.meta.showThirdwebBranding !== false && } + {props.meta?.showThirdwebBranding !== false && } ); diff --git a/packages/thirdweb/src/react/web/wallets/shared/ConnectWalletSocialOptions.tsx b/packages/thirdweb/src/react/web/wallets/shared/ConnectWalletSocialOptions.tsx index 140e92fbf25..440959f74a8 100644 --- a/packages/thirdweb/src/react/web/wallets/shared/ConnectWalletSocialOptions.tsx +++ b/packages/thirdweb/src/react/web/wallets/shared/ConnectWalletSocialOptions.tsx @@ -7,6 +7,8 @@ import type { ThirdwebClient } from "../../../../client/client.js"; import { webLocalStorage } from "../../../../utils/storage/webStorage.js"; import { getEcosystemWalletAuthOptions } from "../../../../wallets/ecosystem/get-ecosystem-wallet-auth-options.js"; import { isEcosystemWallet } from "../../../../wallets/ecosystem/is-ecosystem-wallet.js"; +import type { Profile } from "../../../../wallets/in-app/core/authentication/types.js"; +import { linkProfile } from "../../../../wallets/in-app/core/wallet/profiles.js"; import { loginWithOauthRedirect } from "../../../../wallets/in-app/web/lib/auth/oauth.js"; import type { Account, Wallet } from "../../../../wallets/interfaces/wallet.js"; import { @@ -21,13 +23,13 @@ import { iconSize, spacing, } from "../../../core/design-system/index.js"; +import { setLastAuthProvider } from "../../../core/utils/storage.js"; import { emailIcon, passkeyIcon, phoneIcon, socialIcons, -} from "../../../core/utils/socialIcons.js"; -import { setLastAuthProvider } from "../../../core/utils/storage.js"; +} from "../../../core/utils/walletIcon.js"; import { useSetSelectionData } from "../../providers/wallet-ui-states-provider.js"; import { WalletTypeRowButton } from "../../ui/ConnectWallet/WalletTypeRowButton.js"; import { Img } from "../../ui/components/Img.js"; @@ -47,7 +49,7 @@ export type ConnectWalletSelectUIState = phoneLogin?: string; socialLogin?: { type: SocialAuthOption; - connectionPromise: Promise; + connectionPromise: Promise; }; passkeyLogin?: boolean; }; @@ -70,6 +72,7 @@ export type ConnectWalletSocialOptionsProps = { chain: Chain | undefined; client: ThirdwebClient; size: "compact" | "wide"; + isLinking?: boolean; }; /** @@ -171,7 +174,8 @@ export const ConnectWalletSocialOptions = ( if ( walletConfig && "auth" in walletConfig && - walletConfig?.auth?.mode === "redirect" + walletConfig?.auth?.mode === "redirect" && + !props.isLinking // We do not support redirects for linking ) { return loginWithOauthRedirect({ authOption: strategy, @@ -200,17 +204,18 @@ export const ConnectWalletSocialOptions = ( }, }; - const connectPromise = isEcosystemWallet(wallet) - ? wallet.connect({ - ...connectOptions, - ecosystem: { - id: wallet.id, - partnerId: wallet.getConfig()?.partnerId, - }, - }) - : wallet.connect(connectOptions); - - await setLastAuthProvider(strategy, webLocalStorage); + const connectPromise = (() => { + if (props.isLinking) { + if (wallet.id !== "inApp") { + throw new Error("Only in-app wallets support multi-auth"); + } + return linkProfile(wallet, connectOptions); + } else { + const connectPromise = wallet.connect(connectOptions); + setLastAuthProvider(strategy, webLocalStorage); + return connectPromise; + } + })(); setData({ socialLogin: { diff --git a/packages/thirdweb/src/react/web/wallets/shared/OTPLoginUI.tsx b/packages/thirdweb/src/react/web/wallets/shared/OTPLoginUI.tsx index 9e0882d96b9..b88c1087188 100644 --- a/packages/thirdweb/src/react/web/wallets/shared/OTPLoginUI.tsx +++ b/packages/thirdweb/src/react/web/wallets/shared/OTPLoginUI.tsx @@ -4,7 +4,7 @@ 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 type { SendEmailOtpReturnType } from "../../../../wallets/in-app/core/authentication/types.js"; +import { linkProfile } from "../../../../wallets/in-app/core/wallet/profiles.js"; import { preAuthenticate } from "../../../../wallets/in-app/web/lib/auth/index.js"; import type { Wallet } from "../../../../wallets/interfaces/wallet.js"; import type { EcosystemWalletId } from "../../../../wallets/wallet-types.js"; @@ -24,10 +24,11 @@ import type { InAppWalletLocale } from "./locale/types.js"; type VerificationStatus = | "verifying" | "invalid" + | "linking_error" | "valid" | "idle" | "payment_required"; -type AccountStatus = "sending" | SendEmailOtpReturnType | "error"; +type AccountStatus = "sending" | "sent" | "error"; type ScreenToShow = "base" | "enter-password-or-recovery-code"; /** @@ -42,12 +43,14 @@ export function OTPLoginUI(props: { 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("sending"); const isEcosystem = useMemo(() => isEcosystemWallet(wallet.id), [wallet.id]); @@ -60,7 +63,7 @@ export function OTPLoginUI(props: { try { if ("email" in userInfo) { - const status = await preAuthenticate({ + await preAuthenticate({ ecosystem: isEcosystem ? { id: wallet.id as EcosystemWalletId, @@ -72,9 +75,9 @@ export function OTPLoginUI(props: { strategy: "email", client: props.client, }); - setAccountStatus(status); + setAccountStatus("sent"); } else if ("phone" in userInfo) { - const status = await preAuthenticate({ + await preAuthenticate({ ecosystem: isEcosystem ? { id: wallet.id as EcosystemWalletId, @@ -86,7 +89,7 @@ export function OTPLoginUI(props: { strategy: "phone", client: props.client, }); - setAccountStatus(status); + setAccountStatus("sent"); } else { throw new Error("Invalid userInfo"); } @@ -121,69 +124,52 @@ export function OTPLoginUI(props: { } } - const verify = async (otp: string) => { - if (typeof accountStatus !== "object" || otp.length !== 6) { - return; - } - - setVerifyStatus("idle"); - - if (typeof accountStatus !== "object") { - return; + async function link(otp: string) { + if ("email" in userInfo) { + await linkProfile(wallet as Wallet<"inApp">, { + strategy: "email", + email: userInfo.email, + verificationCode: otp, + }); + } else if ("phone" in userInfo) { + await linkProfile(wallet as Wallet<"inApp">, { + strategy: "phone", + phoneNumber: userInfo.phone, + verificationCode: otp, + }); } + } - if (!wallet) { + const verify = async (otp: string) => { + if (otp.length !== 6) { return; } + setVerifyStatus("verifying"); try { - setVerifyStatus("verifying"); - - const needsRecoveryCode = - accountStatus.recoveryShareManagement === "USER_MANAGED" && - (accountStatus.isNewUser || accountStatus.isNewDevice); - - // USER_MANAGED - if (needsRecoveryCode) { - if (accountStatus.isNewUser) { - try { - await connect(otp); - } catch (e) { - if (e instanceof Error && e.message.includes("encryption key")) { - // setScreen("create-password"); - } else { - throw e; - } - } - } else { - try { - // verifies otp for UI feedback - await connect(otp); - } catch (e) { - if (e instanceof Error && e.message.includes("encryption key")) { - // TODO: do we need this? - // setScreen("enter-password-or-recovery-code"); - } else { - throw e; - } - } - } - } - - // AWS_MANAGED - else { - // verifies otp for UI feedback + // verifies otp for UI feedback + if (props.isLinking) { + await link(otp); + } else { await connect(otp); - done(); } + 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"); } @@ -245,6 +231,15 @@ export function OTPLoginUI(props: { )} + {verifyStatus === "linking_error" && ( + + + + {error || "Failed to verify code"} + + + )} + {verifyStatus === "payment_required" && ( @@ -306,7 +301,7 @@ export function OTPLoginUI(props: { )} - {typeof accountStatus === "object" && ( + {accountStatus === "sent" && ( {locale.emailLoginScreen.resendCode} diff --git a/packages/thirdweb/src/react/web/wallets/shared/PassKeyLogin.tsx b/packages/thirdweb/src/react/web/wallets/shared/PassKeyLogin.tsx index 1ea0ace68e0..3bdb555db15 100644 --- a/packages/thirdweb/src/react/web/wallets/shared/PassKeyLogin.tsx +++ b/packages/thirdweb/src/react/web/wallets/shared/PassKeyLogin.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from "react"; import type { Chain } from "../../../../chains/types.js"; import type { ThirdwebClient } from "../../../../client/client.js"; -import type { Wallet } from "../../../../exports/wallets.js"; +import { type Wallet, linkProfile } from "../../../../exports/wallets.js"; import { webLocalStorage } from "../../../../utils/storage/webStorage.js"; import { isEcosystemWallet } from "../../../../wallets/ecosystem/is-ecosystem-wallet.js"; import { hasStoredPasskey } from "../../../../wallets/in-app/web/lib/auth/passkeys.js"; @@ -9,6 +9,7 @@ import { iconSize } from "../../../core/design-system/index.js"; import { setLastAuthProvider } from "../../../core/utils/storage.js"; import { AccentFailIcon } from "../../ui/ConnectWallet/icons/AccentFailIcon.js"; import { FingerPrintIcon } from "../../ui/ConnectWallet/icons/FingerPrintIcon.js"; +import type { ConnectLocale } from "../../ui/ConnectWallet/locale/types.js"; import { Spacer } from "../../ui/components/Spacer.js"; import { Spinner } from "../../ui/components/Spinner.js"; import { Container, ModalHeader } from "../../ui/components/basic.js"; @@ -23,13 +24,15 @@ import { LoadingScreen } from "./LoadingScreen.js"; export function PassKeyLogin(props: { wallet: Wallet; + locale: ConnectLocale; done: () => void; onBack?: () => void; client: ThirdwebClient; chain: Chain | undefined; size: "compact" | "wide"; + isLinking?: boolean; }) { - const { wallet, done, client, chain, size } = props; + const { wallet, done, client, chain, size, locale } = props; const [screen, setScreen] = useState< "select" | "login" | "loading" | "signup" >("loading"); @@ -60,7 +63,14 @@ export function PassKeyLogin(props: { return ( - + )} @@ -106,6 +117,7 @@ export function PassKeyLogin(props: { client={client} done={done} chain={chain} + isLinking={props.isLinking} /> )} @@ -120,20 +132,32 @@ function LoginScreen(props: { client: ThirdwebClient; onCreate: () => void; chain?: Chain; + isLinking?: boolean; }) { const { wallet, done, client, chain } = props; const [status, setStatus] = useState<"loading" | "error">("loading"); + const [error, setError] = useState(); async function login() { setStatus("loading"); try { - await wallet.connect({ - client: client, - strategy: "passkey", - type: "sign-in", - chain, - }); - await setLastAuthProvider("passkey", webLocalStorage); + if (props.isLinking) { + await linkProfile(wallet as Wallet<"inApp">, { + strategy: "passkey", + type: "sign-in", + }).catch((e) => { + setError(e.message); + throw e; + }); + } else { + await wallet.connect({ + client: client, + strategy: "passkey", + type: "sign-in", + chain, + }); + await setLastAuthProvider("passkey", webLocalStorage); + } done(); } catch { setStatus("error"); @@ -162,7 +186,7 @@ function LoginScreen(props: { if (status === "error") { return ( <> - +