From 4b864cb1694bc92f1a39b845faf1282f3b243956 Mon Sep 17 00:00:00 2001 From: Manan Tank Date: Thu, 22 Feb 2024 00:14:26 +0530 Subject: [PATCH] Add EmbeddedWallet UI --- packages/thirdweb/.eslintrc.cjs | 2 +- packages/thirdweb/src/exports/react.ts | 5 + packages/thirdweb/src/exports/wallets.ts | 1 + packages/thirdweb/src/react/types/wallets.ts | 78 +++- .../Modal/ConnectModalContent.tsx | 40 +- .../react/ui/ConnectWallet/WalletSelector.tsx | 49 ++- .../react/wallets/coinbase/coinbaseConfig.tsx | 2 +- .../wallets/embedded/EmbeddedWalletFormUI.tsx | 242 ++++++++++++ .../embedded/EmbeddedWalletOTPLoginUI.tsx | 358 ++++++++++++++++++ .../embedded/EmbeddedWalletSocialLogin.tsx | 137 +++++++ .../wallets/embedded/InputSelectionUI.tsx | 94 +++++ .../wallets/embedded/embeddedWalletConfig.tsx | 180 +++++++++ .../wallets/embedded/openOauthSignInWindow.ts | 117 ++++++ .../src/react/wallets/embedded/socialIcons.ts | 11 + .../src/react/wallets/embedded/types.ts | 18 + .../src/react/wallets/headlessConnectUI.tsx | 3 +- .../wallets/shared/InjectedConnectUI.tsx | 7 +- .../shared/WalletConnectConnection.tsx | 8 +- .../smartWallet/SmartWalletConnectUI.tsx | 26 +- .../walletConnect/walletConnectConfig.tsx | 16 +- .../src/wallets/embedded/core/wallet/index.ts | 130 ++++++- .../src/wallets/embedded/core/wallet/types.ts | 2 - .../thirdweb/src/wallets/injected/index.ts | 2 +- packages/thirdweb/tsdoc.json | 7 +- 24 files changed, 1444 insertions(+), 91 deletions(-) create mode 100644 packages/thirdweb/src/react/wallets/embedded/EmbeddedWalletFormUI.tsx create mode 100644 packages/thirdweb/src/react/wallets/embedded/EmbeddedWalletOTPLoginUI.tsx create mode 100644 packages/thirdweb/src/react/wallets/embedded/EmbeddedWalletSocialLogin.tsx create mode 100644 packages/thirdweb/src/react/wallets/embedded/InputSelectionUI.tsx create mode 100644 packages/thirdweb/src/react/wallets/embedded/embeddedWalletConfig.tsx create mode 100644 packages/thirdweb/src/react/wallets/embedded/openOauthSignInWindow.ts create mode 100644 packages/thirdweb/src/react/wallets/embedded/socialIcons.ts create mode 100644 packages/thirdweb/src/react/wallets/embedded/types.ts diff --git a/packages/thirdweb/.eslintrc.cjs b/packages/thirdweb/.eslintrc.cjs index 6f509753597..3d5bc77a1ff 100644 --- a/packages/thirdweb/.eslintrc.cjs +++ b/packages/thirdweb/.eslintrc.cjs @@ -31,7 +31,7 @@ const jsdocRuleOverrides = { "extension", "rpc", "transaction", - "wallet", + "walletConfig", "connectWallet", "theme", "locale", diff --git a/packages/thirdweb/src/exports/react.ts b/packages/thirdweb/src/exports/react.ts index 514e4c68c91..a9b9a1f0495 100644 --- a/packages/thirdweb/src/exports/react.ts +++ b/packages/thirdweb/src/exports/react.ts @@ -82,6 +82,11 @@ export { type SmartWalletConfigOptions, } from "../react/wallets/smartWallet/smartWalletConfig.js"; +export { + embeddedWalletConfig, + type EmbeddedWalletConfigOptions, +} from "../react/wallets/embedded/embeddedWalletConfig.js"; + export type { SupportedTokens } from "../react/ui/ConnectWallet/defaultTokens.js"; export { defaultTokens } from "../react/ui/ConnectWallet/defaultTokens.js"; diff --git a/packages/thirdweb/src/exports/wallets.ts b/packages/thirdweb/src/exports/wallets.ts index 0414ae631a3..c71e648c9d3 100644 --- a/packages/thirdweb/src/exports/wallets.ts +++ b/packages/thirdweb/src/exports/wallets.ts @@ -94,6 +94,7 @@ export { export { coinbaseMetadata } from "../wallets/coinbase/coinbaseMetadata.js"; export { embeddedWallet } from "../wallets/embedded/core/wallet/index.js"; +export { embeddedWalletMetadata } from "../wallets/embedded/core/wallet/index.js"; export { type MultiStepAuthArgsType, type SingleStepAuthArgsType, diff --git a/packages/thirdweb/src/react/types/wallets.ts b/packages/thirdweb/src/react/types/wallets.ts index 78c6275d2d1..ea08face769 100644 --- a/packages/thirdweb/src/react/types/wallets.ts +++ b/packages/thirdweb/src/react/types/wallets.ts @@ -4,7 +4,7 @@ import type { Wallet } from "../../wallets/interfaces/wallet.js"; import type { DAppMetaData } from "../../wallets/types.js"; /** - * @wallet + * @walletConfig */ export type WalletConfig = { category?: "socialLogin" | "walletLogin"; @@ -65,6 +65,9 @@ export type WalletConfig = { personalWalletConfigs?: WalletConfig[]; }; +/** + * @walletConfig + */ export type ScreenConfig = { /** * Hide or show the Modal that the screen is rendered in. This is useful if you want to open up another Modal as part of the wallet connection process and want to hide the current Modal to avoid showing multiple Modals at the same time @@ -97,20 +100,9 @@ export type ScreenConfig = { }; /** - * @wallet + * @walletConfig */ -export type ConnectUIProps = { - /** - * The wallet config object of the wallet - * You can use this to use the wallet's properties / methods like `metadata`, `create`, `isInstalled` etc in your UI - */ - walletConfig: WalletConfig; - - /** - * Information about the screen that the wallet's UI and functions to control certain aspects of the screen - */ - screenConfig: ScreenConfig; - +export type WalletConfigConnection = { /** * when wallet connection is complete, call the `complete` function with the `wallet` instance */ @@ -134,21 +126,71 @@ export type ConnectUIProps = { }; /** - * @wallet + * @walletConfig */ -export type SelectUIProps = { +export type WalletConfigSelection = { /** * Call this function to "select" your wallet and move to next step of showing the "connectUI" */ select: () => void; + /** + * This is true if there are no other wallets to select from and this wallet is the only option + */ + isSingularOption: boolean; + + /** + * Arbitrary data saved in Context by `selection.saveData` + */ + data: any; + + /** + * Save Arbitrary data in Context which later can be accessed via `selection.data` + */ + saveData: (data: any) => void; +}; + +/** + * Props provided to the `WalletConfig.connectUI` component + * @walletConfig + */ +export type ConnectUIProps = { + /** + * The wallet config object of the wallet + * You can use this to use the wallet's properties / methods like `metadata`, `create`, `isInstalled` etc in your UI + */ + walletConfig: WalletConfig; + /** * Information about the screen that the wallet's UI and functions to control certain aspects of the screen */ screenConfig: ScreenConfig; /** - * This is true if there are no other wallets to select from and this wallet is the only option + * Methods and properties for wallet connection */ - isSingularOption: boolean; + connection: WalletConfigConnection; + + selection: { + /** + * Arbitrary data saved in Context by `selection.saveData` + */ + data: any; + + /** + * Save Arbitrary data in Context which can be accessed via `selection.data` in connect UI + */ + saveData: (data: any) => void; + }; +}; + +/** + * Props provided to the `WalletConfig.selectUI` component + * @walletConfig + */ +export type SelectUIProps = ConnectUIProps & { + /** + * Methods and properties for wallet selection + */ + selection: WalletConfigSelection; }; diff --git a/packages/thirdweb/src/react/ui/ConnectWallet/Modal/ConnectModalContent.tsx b/packages/thirdweb/src/react/ui/ConnectWallet/Modal/ConnectModalContent.tsx index 3965c9d808c..8a7b0457884 100644 --- a/packages/thirdweb/src/react/ui/ConnectWallet/Modal/ConnectModalContent.tsx +++ b/packages/thirdweb/src/react/ui/ConnectWallet/Modal/ConnectModalContent.tsx @@ -1,5 +1,6 @@ import { ModalConfigCtx, + SetModalConfigCtx, // SetModalConfigCtx, } from "../../../providers/wallet-ui-states-provider.js"; import { useCallback, useContext } from "react"; @@ -32,7 +33,7 @@ export const ConnectModalContent = (props: { const { wallets, client, dappMetadata } = useThirdwebProviderProps(); // const disconnect = useDisconnect(); const modalConfig = useContext(ModalConfigCtx); - // const setModalConfig = useContext(SetModalConfigCtx); + const setModalConfig = useContext(SetModalConfigCtx); // const activeWalletConnectionStatus = useActiveWalletConnectionStatus(); // const setActiveWalletConnectionStatus = useSetActiveWalletConnectionStatus(); // const activeWallet = useActiveWallet(); @@ -44,6 +45,16 @@ export const ConnectModalContent = (props: { const onConnect = modalConfig.onConnect; const isWideModal = modalSize === "wide"; + const saveData = useCallback( + (data: any) => { + setModalConfig((prev) => ({ + ...prev, + data: data, + })); + }, + [setModalConfig], + ); + // const { user } = useUser(); // const authConfig = useThirdwebAuthContext(); @@ -121,6 +132,12 @@ export const ConnectModalContent = (props: { size: modalConfig.modalSize, }; + const connection = { + done: handleConnected, + chain: modalConfig.chain, + chains: modalConfig.chains, + }; + const walletList = ( ); @@ -144,15 +162,19 @@ export const ConnectModalContent = (props: { { - return walletConfig.create({ - client, - dappMetadata, - }); + connection={{ + ...connection, + createInstance: () => { + return walletConfig.create({ + client, + dappMetadata, + }); + }, + }} + selection={{ + data: modalConfig.data, + saveData, }} - chains={modalConfig.chains} - chain={modalConfig.chain} /> ); }; diff --git a/packages/thirdweb/src/react/ui/ConnectWallet/WalletSelector.tsx b/packages/thirdweb/src/react/ui/ConnectWallet/WalletSelector.tsx index 976b7cde353..ff0df91b69f 100644 --- a/packages/thirdweb/src/react/ui/ConnectWallet/WalletSelector.tsx +++ b/packages/thirdweb/src/react/ui/ConnectWallet/WalletSelector.tsx @@ -1,8 +1,9 @@ import { ChevronLeftIcon } from "@radix-ui/react-icons"; -import { useContext, useState, useRef, useEffect } from "react"; +import { useContext, useState, useRef, useEffect, useCallback } from "react"; import { useTWLocale } from "../../providers/locale-provider.js"; import { ModalConfigCtx, + SetModalConfigCtx, // SetModalConfigCtx, } from "../../providers/wallet-ui-states-provider.js"; import type { WalletConfig, SelectUIProps } from "../../types/wallets.js"; @@ -27,14 +28,11 @@ import { TWIcon } from "./icons/twIcon.js"; import { Text } from "../components/text.js"; import { PoweredByThirdweb } from "./PoweredByTW.js"; import { useScreenContext } from "./Modal/screen.js"; +import { useThirdwebProviderProps } from "../../hooks/others/useThirdwebProviderProps.js"; type WalletSelectUIProps = { screenConfig: SelectUIProps["screenConfig"]; - // activeWalletConnectionStatus: SelectUIProps["activeWalletConnectionStatus"]; - // connected: SelectUIProps["connected"]; - // setActiveWalletConnectionStatus: SelectUIProps["setActiveWalletConnectionStatus"]; - // activeWallet?: SelectUIProps["activeWallet"]; - // activeWalletAddress?: SelectUIProps["activeWalletAddress"]; + connection: Omit; }; // temp @@ -446,9 +444,19 @@ const WalletSelection: React.FC<{ maxHeight?: string; selectUIProps: WalletSelectUIProps; }> = (props) => { - // const modalConfig = useContext(ModalConfigCtx); - // const setModalConfig = useContext(SetModalConfigCtx); + const { client, dappMetadata } = useThirdwebProviderProps(); + const modalConfig = useContext(ModalConfigCtx); + const setModalConfig = useContext(SetModalConfigCtx); const walletConfigs = sortWalletConfigs(props.walletConfigs); + const saveData = useCallback( + (data: any) => { + setModalConfig({ + ...modalConfig, + data, + }); + }, + [modalConfig, setModalConfig], + ); return ( @@ -461,13 +469,24 @@ const WalletSelection: React.FC<{ {walletConfig.selectUI ? ( { - props.selectWallet(walletConfig); - // setModalConfig((config) => ({ ...config, data })); + selection={{ + select: () => { + props.selectWallet(walletConfig); + }, + isSingularOption: walletConfigs.length === 1, + data: modalConfig.data, + saveData: saveData, + }} + connection={{ + ...props.selectUIProps.connection, + createInstance: () => { + return walletConfig.create({ + client, + dappMetadata, + }); + }, }} - isSingularOption={walletConfigs.length === 1} - // {...props.selectUIProps} - // connect={walletConfig.connect} + walletConfig={walletConfig} /> ) : ( void; }) { diff --git a/packages/thirdweb/src/react/wallets/coinbase/coinbaseConfig.tsx b/packages/thirdweb/src/react/wallets/coinbase/coinbaseConfig.tsx index 691dd2f0a48..7314bc17278 100644 --- a/packages/thirdweb/src/react/wallets/coinbase/coinbaseConfig.tsx +++ b/packages/thirdweb/src/react/wallets/coinbase/coinbaseConfig.tsx @@ -108,7 +108,7 @@ function CoinbaseSDKWalletConnectUI(props: { const locale = useTWLocale().wallets.injectedWallet( connectUIProps.walletConfig.metadata.name, ); - const { createInstance, done, chain } = connectUIProps; + const { createInstance, done, chain } = connectUIProps.connection; const [qrCodeUri, setQrCodeUri] = useState(undefined); const scanStarted = useRef(false); diff --git a/packages/thirdweb/src/react/wallets/embedded/EmbeddedWalletFormUI.tsx b/packages/thirdweb/src/react/wallets/embedded/EmbeddedWalletFormUI.tsx new file mode 100644 index 00000000000..cde20f5c312 --- /dev/null +++ b/packages/thirdweb/src/react/wallets/embedded/EmbeddedWalletFormUI.tsx @@ -0,0 +1,242 @@ +import styled from "@emotion/styled"; +import type { ConnectUIProps } from "../../types/wallets.js"; +import { useContext } from "react"; +import type { EmbeddedWallet } from "../../../wallets/embedded/core/wallet/index.js"; +import { useTWLocale } from "../../providers/locale-provider.js"; +import { ModalConfigCtx } from "../../providers/wallet-ui-states-provider.js"; +import { TOS } from "../../ui/ConnectWallet/Modal/TOS.js"; +import { useScreenContext } from "../../ui/ConnectWallet/Modal/screen.js"; +import { PoweredByThirdweb } from "../../ui/ConnectWallet/PoweredByTW.js"; +import { Img } from "../../ui/components/Img.js"; +import { Spacer } from "../../ui/components/Spacer.js"; +import { TextDivider } from "../../ui/components/TextDivider.js"; +import { Container, ModalHeader } from "../../ui/components/basic.js"; +import { Button } from "../../ui/components/buttons.js"; +import { useCustomTheme } from "../../ui/design-system/CustomThemeProvider.js"; +import { iconSize, spacing, fontSize } from "../../ui/design-system/index.js"; +import { socialIcons } from "./socialIcons.js"; +import type { + EmbeddedWalletAuth, + EmbeddedWalletSelectUIState, + EmbeddedWalletSocialAuth, +} from "./types.js"; +import { openOauthSignInWindow } from "./openOauthSignInWindow.js"; +import { InputSelectionUI } from "./InputSelectionUI.js"; + +export type EmbeddedWalletFormUIProps = { + connectUIProps: ConnectUIProps; + authOptions: EmbeddedWalletAuth[]; + saveState: (state: EmbeddedWalletSelectUIState) => void; + select: () => void; +}; + +/** + * @internal + */ +export const EmbeddedWalletFormUI = (props: EmbeddedWalletFormUIProps) => { + const twLocale = useTWLocale(); + const locale = twLocale.wallets.embeddedWallet; + + const { screenConfig } = props.connectUIProps; + const { done, createInstance, chain } = props.connectUIProps.connection; + + const themeObj = useCustomTheme(); + + const loginMethodsLabel = { + google: locale.signInWithGoogle, + facebook: locale.signInWithFacebook, + apple: locale.signInWithApple, + }; + + const enableEmailLogin = true; + // const enableEmailLogin = props.authOptions.includes("email"); + + const socialLogins = props.authOptions.filter( + (x) => x !== "email", + ) as EmbeddedWalletSocialAuth[]; + + const hasSocialLogins = socialLogins.length > 0; + + // Need to trigger login on button click to avoid popup from being blocked + const handleSocialLogin = async (strategy: EmbeddedWalletSocialAuth) => { + try { + const wallet = createInstance() as EmbeddedWallet; + + const socialLoginWindow = openOauthSignInWindow(strategy, themeObj); + if (!socialLoginWindow) { + throw new Error("Failed to open login window"); + } + + const connectPromise = wallet.connect({ + chain, + strategy: strategy, + openedWindow: socialLoginWindow, + closeOpenedWindow: (openedWindow) => { + openedWindow.close(); + }, + }); + + props.saveState({ + socialLogin: { + wallet, + type: strategy, + connectionPromise: connectPromise, + }, + }); + props.select(); + + await connectPromise; + + done(wallet); + } catch (e) { + console.error(`Error sign in with ${strategy}`, e); + } + }; + + const showOnlyIcons = socialLogins.length > 1; + + return ( + + {/* Social Login */} + {hasSocialLogins && ( + + {socialLogins.map((loginMethod) => { + const imgIconSize = showOnlyIcons ? iconSize.lg : iconSize.md; + return ( + { + handleSocialLogin(loginMethod); + }} + > + + {!showOnlyIcons && loginMethodsLabel[loginMethod]} + + ); + })} + + )} + + {screenConfig.size === "wide" && hasSocialLogins && enableEmailLogin && ( + + )} + + {/* Email Login */} + {enableEmailLogin && ( + { + props.saveState({ + emailLogin: email, + }); + props.select(); + }} + placeholder={locale.emailPlaceholder} + name="email" + type="email" + errorMessage={(_input) => { + const input = _input.replace(/\+/g, "").toLowerCase(); + const emailRegex = + /^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,})$/g; + const isValidEmail = emailRegex.test(input); + if (!isValidEmail) { + return locale.invalidEmail; + } + + return undefined; + }} + emptyErrorMessage={locale.emailRequired} + submitButtonText={locale.submitEmail} + /> + )} + + ); +}; + +/** + * @internal + */ +export function EmbeddedWalletFormUIScreen(props: EmbeddedWalletFormUIProps) { + const locale = useTWLocale().wallets.embeddedWallet.emailLoginScreen; + const isCompact = props.connectUIProps.screenConfig.size === "compact"; + const { initialScreen, screen } = useScreenContext(); + const modalConfig = useContext(ModalConfigCtx); + const walletConfig = props.connectUIProps.walletConfig; + const { goBack } = props.connectUIProps.screenConfig; + + return ( + + + {isCompact ? : null} + + + + + + {isCompact && + (modalConfig.showThirdwebBranding !== false || + modalConfig.termsOfServiceUrl || + modalConfig.privacyPolicyUrl) && } + + + + + {modalConfig.showThirdwebBranding !== false && } + + + ); +} + +const SocialButton = /* @__PURE__ */ styled(Button)({ + "&[data-variant='full']": { + display: "flex", + justifyContent: "center", + gap: spacing.md, + fontSize: fontSize.md, + transition: "background-color 0.2s ease", + "&:active": { + boxShadow: "none", + }, + }, + "&[data-variant='icon']": { + padding: spacing.sm, + flexGrow: 1, + }, +}); diff --git a/packages/thirdweb/src/react/wallets/embedded/EmbeddedWalletOTPLoginUI.tsx b/packages/thirdweb/src/react/wallets/embedded/EmbeddedWalletOTPLoginUI.tsx new file mode 100644 index 00000000000..9490f54645c --- /dev/null +++ b/packages/thirdweb/src/react/wallets/embedded/EmbeddedWalletOTPLoginUI.tsx @@ -0,0 +1,358 @@ +import { useState, useCallback, useRef, useEffect } from "react"; +import type { EmbeddedWallet } from "../../../wallets/embedded/core/wallet/index.js"; +import type { SendEmailOtpReturnType } from "../../../wallets/embedded/implementations/index.js"; +import { useTWLocale } from "../../providers/locale-provider.js"; +import type { ConnectUIProps } from "../../types/wallets.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 { Container, ModalHeader, Line } from "../../ui/components/basic.js"; +import { Button } from "../../ui/components/buttons.js"; +import { useCustomTheme } from "../../ui/design-system/CustomThemeProvider.js"; +import { StyledButton } from "../../ui/design-system/elements.js"; +import { fontSize } from "../../ui/design-system/index.js"; +import { Text } from "../../ui/components/text.js"; + +type VerificationStatus = + | "verifying" + | "invalid" + | "valid" + | "idle" + | "payment_required"; +type EmailStatus = "sending" | SendEmailOtpReturnType | "error"; +type ScreenToShow = + | "base" + | "create-password" + | "enter-password-or-recovery-code"; + +/** + * @internal + */ +export function EmbeddedWalletOTPLoginUI(props: { + connectUIProps: ConnectUIProps; + email: string; +}) { + const email = props.email; + const isWideModal = props.connectUIProps.screenConfig.size === "wide"; + const locale = useTWLocale().wallets.embeddedWallet; + const [otpInput, setOtpInput] = useState(""); + const { createInstance, done, chain } = props.connectUIProps.connection; + const { goBack } = props.connectUIProps.screenConfig; + + const [wallet, setWallet] = useState(null); + const [verifyStatus, setVerifyStatus] = useState("idle"); + const [emailStatus, setEmailStatus] = useState("sending"); + + const [screen] = useState("base"); + + const sendEmail = useCallback(async () => { + setOtpInput(""); + setVerifyStatus("idle"); + setEmailStatus("sending"); + + try { + const _wallet = createInstance() as EmbeddedWallet; + setWallet(_wallet); + const status = await _wallet.preAuthenticate({ + email, + strategy: "email", + }); + setEmailStatus(status); + } catch (e) { + console.error(e); + setVerifyStatus("idle"); + setEmailStatus("error"); + } + }, [createInstance, email]); + + const verify = async (otp: string) => { + if (typeof emailStatus !== "object" || otp.length !== 6) { + return; + } + + setVerifyStatus("idle"); + + if (typeof emailStatus !== "object") { + return; + } + + if (!wallet) { + return; + } + + try { + setVerifyStatus("verifying"); + + const needsRecoveryCode = + emailStatus.recoveryShareManagement === "USER_MANAGED" && + (emailStatus.isNewUser || emailStatus.isNewDevice); + + // USER_MANAGED + if (needsRecoveryCode) { + if (emailStatus.isNewUser) { + try { + await wallet.connect({ + chain, + strategy: "email", + email, + verificationCode: otp, + }); + } catch (e: any) { + if (e instanceof Error && e.message.includes("encryption key")) { + // TODO: do we need this? + // setScreen("create-password"); + } else { + throw e; + } + } + } else { + try { + // verifies otp for UI feedback + await wallet.connect({ + chain, + strategy: "email", + email, + verificationCode: otp, + }); + } catch (e: any) { + 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 { + const authResult = await wallet.connect({ + chain, + strategy: "email", + email, + verificationCode: otp, + }); + if (!authResult) { + throw new Error("Failed to verify OTP"); + } + + done(wallet); + } + + setVerifyStatus("valid"); + } catch (e: any) { + if (e?.message?.includes("PAYMENT_METHOD_REQUIRED")) { + setVerifyStatus("payment_required"); + } else { + setVerifyStatus("invalid"); + } + console.error("Authentication Error", e); + } + }; + + // send email on mount + const emailSentOnMount = useRef(false); + useEffect(() => { + if (!emailSentOnMount.current) { + emailSentOnMount.current = true; + sendEmail(); + } + }, [sendEmail]); + + // if (screen === "create-password") { + // return ( + // { + // if (!wallet || typeof emailStatus !== "object") { + // return; + // } + // const authResult = await wallet.connect({ + // chain, + // strategy: "email", + // email, + // verificationCode: otpInput, + // // recoveryCode: password, + // }); + // if (!authResult) { + // throw new Error("Failed to verify recovery code"); + // } + // await wallet.connect({ + // authResult, + // }); + // setConnectedWallet(wallet); + // props.connected(); + // }} + // /> + // ); + // } + + // if (screen === "enter-password-or-recovery-code") { + // return ( + // { + // if (!wallet || typeof emailStatus !== "object") { + // return; + // } + // const authResult = await wallet.authenticate({ + // strategy: "email_verification", + // email, + // verificationCode: otpInput, + // recoveryCode: passwordOrRecoveryCode, + // }); + // if (!authResult) { + // throw new Error("Failed to verify recovery code"); + // } + // await wallet.connect({ + // authResult, + // }); + + // setConnectedWallet(wallet); + // props.connected(); + // }} + // /> + // ); + // } + + if (screen === "base") { + return ( + + + + + + +
{ + e.preventDefault(); + }} + > + + {!isWideModal && } + {locale.emailLoginScreen.enterCodeSendTo} + + {email} + + + + { + setOtpInput(value); + setVerifyStatus("idle"); // reset error + verify(value); + }} + onEnter={() => { + verify(otpInput); + }} + /> + + {verifyStatus === "invalid" && ( + + + + {locale.emailLoginScreen.invalidCode} + + + )} + + {verifyStatus === "payment_required" && ( + + + + {locale.maxAccountsExceeded} + + + )} + + + + + {verifyStatus === "verifying" ? ( + <> + + + + + ) : ( + + + + )} + + + + + {!isWideModal && } + + + {emailStatus === "error" && ( + <> + + {locale.emailLoginScreen.failedToSendCode} + + + )} + + {emailStatus === "sending" && ( + + {locale.emailLoginScreen.sendingCode} + + + )} + + {typeof emailStatus === "object" && ( + + {locale.emailLoginScreen.resendCode} + + )} + + +
+
+ ); + } + + return null; +} + +const LinkButton = /* @__PURE__ */ StyledButton(() => { + const theme = useCustomTheme(); + return { + all: "unset", + color: theme.colors.accentText, + fontSize: fontSize.sm, + cursor: "pointer", + textAlign: "center", + fontWeight: 500, + width: "100%", + "&:hover": { + color: theme.colors.primaryText, + }, + }; +}); diff --git a/packages/thirdweb/src/react/wallets/embedded/EmbeddedWalletSocialLogin.tsx b/packages/thirdweb/src/react/wallets/embedded/EmbeddedWalletSocialLogin.tsx new file mode 100644 index 00000000000..6c18fc8fa1f --- /dev/null +++ b/packages/thirdweb/src/react/wallets/embedded/EmbeddedWalletSocialLogin.tsx @@ -0,0 +1,137 @@ +import { useState, useEffect } from "react"; +import { useTWLocale } from "../../providers/locale-provider.js"; +import type { ConnectUIProps } from "../../types/wallets.js"; +import { Spacer } from "../../ui/components/Spacer.js"; +import { Spinner } from "../../ui/components/Spinner.js"; +import { Container, ModalHeader } from "../../ui/components/basic.js"; +import { Button } from "../../ui/components/buttons.js"; +import { useCustomTheme } from "../../ui/design-system/CustomThemeProvider.js"; +import { openOauthSignInWindow } from "./openOauthSignInWindow.js"; +import type { + EmbeddedWalletSelectUIState, + EmbeddedWalletSocialAuth, +} from "./types.js"; +import type { EmbeddedWallet } from "../../../wallets/embedded/core/wallet/index.js"; +import { Text } from "../../ui/components/text.js"; + +/** + * @internal + */ +export function EmbeddedWalletSocialLogin(props: { + connectUIProps: ConnectUIProps; + socialAuth: EmbeddedWalletSocialAuth; + state: EmbeddedWalletSelectUIState; +}) { + const ewLocale = useTWLocale().wallets.embeddedWallet; + const locale = ewLocale.socialLoginScreen; + const themeObj = useCustomTheme(); + const [authError, setAuthError] = useState(undefined); + const { createInstance, done, chain } = props.connectUIProps.connection; + const [isConnecting, setIsConnecting] = useState(false); + const { goBack, size } = props.connectUIProps.screenConfig; + + const handleSocialLogin = async () => { + try { + const wallet = createInstance() as EmbeddedWallet; + const socialWindow = openOauthSignInWindow(props.socialAuth, themeObj); + + if (!socialWindow) { + throw new Error(`Failed to open ${props.socialAuth} login window`); + } + + await wallet.connect({ + chain, + strategy: props.socialAuth, + openedWindow: socialWindow, + closeOpenedWindow: (openedWindow) => { + openedWindow.close(); + }, + }); + + done(wallet); + } catch (e: any) { + // TODO this only happens on 'retry' button click, not on initial login + // should pass auth error message to this component + if (e?.message?.includes("PAYMENT_METHOD_REQUIRED")) { + setAuthError(ewLocale.maxAccountsExceeded); + } + console.error(`Error sign in with ${props.socialAuth}`, e); + } + }; + + const { setModalVisibility } = props.connectUIProps.screenConfig; + const socialLogin = props.state?.socialLogin; + + useEffect(() => { + if (socialLogin) { + setIsConnecting(true); + socialLogin.connectionPromise + .then(() => { + done(socialLogin.wallet); + }) + .catch(() => { + setIsConnecting(false); + }); + } + }, [done, setModalVisibility, socialLogin]); + + return ( + + + + + {size === "compact" ? : null} + + + {isConnecting && ( + + + {locale.instruction} + + + + + + + + + )} + + {!isConnecting && ( + + {locale.failed} + {authError && {authError}} + + + + + )} + + + + ); +} diff --git a/packages/thirdweb/src/react/wallets/embedded/InputSelectionUI.tsx b/packages/thirdweb/src/react/wallets/embedded/InputSelectionUI.tsx new file mode 100644 index 00000000000..1e4e9eb76f2 --- /dev/null +++ b/packages/thirdweb/src/react/wallets/embedded/InputSelectionUI.tsx @@ -0,0 +1,94 @@ +import { useState } from "react"; +import { Spacer } from "../../ui/components/Spacer.js"; +import { Button } from "../../ui/components/buttons.js"; +import { Input } from "../../ui/components/formElements.js"; +import { Text } from "../../ui/components/text.js"; + +/** + * @internal + */ +export function InputSelectionUI(props: { + onSelect: (data: string) => void; + placeholder: string; + name: string; + type: string; + errorMessage?: (input: string) => string | undefined; + emptyErrorMessage?: string; + submitButtonText: string; +}) { + const [input, setInput] = useState(""); + const [error, setError] = useState(); + const [showError, setShowError] = useState(false); + + const handleSelect = () => { + setShowError(true); + if (!input || !!error) { + return; + } + + props.onSelect(input); + }; + + const renderingError = + (showError && !!error) || + (!input && !!props.emptyErrorMessage && showError); + + return ( +
+
+ { + setInput(e.target.value); + if (props.errorMessage) { + setError(props.errorMessage(e.target.value)); + } else { + setError(undefined); + } + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + handleSelect(); + } + }} + /> +
+ + {showError && error && ( + <> + + + {error} + + + )} + + {!(showError && error) && + !input && + props.emptyErrorMessage && + showError && ( + <> + + + {props.emptyErrorMessage} + + + )} + + + +
+ ); +} diff --git a/packages/thirdweb/src/react/wallets/embedded/embeddedWalletConfig.tsx b/packages/thirdweb/src/react/wallets/embedded/embeddedWalletConfig.tsx new file mode 100644 index 00000000000..2d7be9f6da2 --- /dev/null +++ b/packages/thirdweb/src/react/wallets/embedded/embeddedWalletConfig.tsx @@ -0,0 +1,180 @@ +import { + embeddedWalletMetadata, + embeddedWallet, +} from "../../../wallets/embedded/core/wallet/index.js"; +import type { + ConnectUIProps, + SelectUIProps, + WalletConfig, +} from "../../types/wallets.js"; +import { useScreenContext } from "../../ui/ConnectWallet/Modal/screen.js"; +import { WalletEntryButton } from "../../ui/ConnectWallet/WalletSelector.js"; +import { reservedScreens } from "../../ui/ConnectWallet/constants.js"; +import { + EmbeddedWalletFormUI, + EmbeddedWalletFormUIScreen, +} from "./EmbeddedWalletFormUI.js"; +import { EmbeddedWalletOTPLoginUI } from "./EmbeddedWalletOTPLoginUI.js"; +import { EmbeddedWalletSocialLogin } from "./EmbeddedWalletSocialLogin.js"; +import type { + EmbeddedWalletAuth, + EmbeddedWalletSelectUIState, +} from "./types.js"; + +export type EmbeddedWalletConfigOptions = { + recommended?: boolean; + auth: { + options: EmbeddedWalletAuth[]; + }; +}; + +/** + * Integrate Embedded wallet into your app. + * @param options - Options for configuring the Embedded wallet. + * @example + * ```tsx + * + * wallets={[ embeddedWalletConfig() ]} + * + * + * ``` + * @returns WalletConfig object to be passed into `ThirdwebProvider` + */ +export const embeddedWalletConfig = ( + options?: EmbeddedWalletConfigOptions, +): WalletConfig => { + const defaultAuthOptions: EmbeddedWalletAuth[] = [ + "email", + "google", + "apple", + "facebook", + ]; + const authOptions = options?.auth.options || defaultAuthOptions; + const hasEmail = authOptions.includes("email"); + const hasSocial = + (hasEmail && authOptions.length > 1) || + (!hasEmail && authOptions.length > 0); + + const config: WalletConfig = { + category: "socialLogin", + metadata: { + ...embeddedWalletMetadata, + name: + hasEmail && hasSocial + ? "Email & Social Login" + : hasEmail + ? "Email" + : hasSocial + ? "Social Login" + : "Embedded Wallet", + }, + create(createOptions) { + return embeddedWallet({ + client: createOptions.client, + }); + }, + selectUI(props) { + return ( + + ); + }, + connectUI(props) { + return ( + + ); + }, + }; + + return config; +}; + +function EmbeddedWalletSelectionUI(props: { + selectUIProps: SelectUIProps; + saveState: (state: EmbeddedWalletSelectUIState) => void; + authOptions: EmbeddedWalletAuth[]; + select: () => void; +}) { + const { screen } = useScreenContext(); + const { size } = props.selectUIProps.screenConfig; + const { walletConfig } = props.selectUIProps; + + // do not show the "selectUI" if + // modal is compact or + // it is being rendered in Safe wallet + if ( + size === "wide" || + (screen !== reservedScreens.main && size === "compact") + ) { + return ( + { + props.saveState({}); + props.select(); + }} + /> + ); + } + + return ( + + ); +} + +function EmbeddedWalletConnectUI(props: { + connectUIProps: ConnectUIProps; + authOptions: EmbeddedWalletAuth[]; +}) { + const state = props.connectUIProps.selection + .data as EmbeddedWalletSelectUIState; + + if (state?.emailLogin) { + return ( + + ); + } + + if (state?.socialLogin) { + return ( + + ); + } + + return ( + {}} + /> + ); +} diff --git a/packages/thirdweb/src/react/wallets/embedded/openOauthSignInWindow.ts b/packages/thirdweb/src/react/wallets/embedded/openOauthSignInWindow.ts new file mode 100644 index 00000000000..9d08cde6998 --- /dev/null +++ b/packages/thirdweb/src/react/wallets/embedded/openOauthSignInWindow.ts @@ -0,0 +1,117 @@ +import type { EmbeddedWalletSocialAuth } from "./types.js"; +import type { Theme } from "../../ui/design-system/index.js"; + +function getBodyTitle(authOption: EmbeddedWalletSocialAuth) { + switch (authOption) { + case "google": + return "Sign In - Google Accounts"; + default: + return `Sign In - ${authOption + .slice(0, 1) + .toUpperCase()}${authOption.slice(1)}`; + } +} + +function getWidthAndHeight(authOption: EmbeddedWalletSocialAuth) { + switch (authOption) { + case "facebook": + return { width: 715, height: 555 }; + default: + return { width: 350, height: 500 }; + } +} + +/** + * + * @internal + */ +export function openOauthSignInWindow( + authOption: EmbeddedWalletSocialAuth, + themeObj: Theme, +) { + // open the popup in the center of the screen + const { height, width } = getWidthAndHeight(authOption); + const top = (window.innerHeight - height) / 2; + const left = (window.innerWidth - width) / 2; + + const win = window.open( + "", + undefined, + `width=${width}, height=${height}, top=${top}, left=${left}`, + ); + if (win) { + const title = getBodyTitle(authOption); + win.document.title = title; + win.document.body.innerHTML = spinnerWindowHtml; + win.document.body.style.background = themeObj.colors.modalBg; + win.document.body.style.color = themeObj.colors.accentText; + } + + // close it when current window is closed or refreshed + if (win) { + window.addEventListener("beforeunload", () => { + win?.close(); + }); + } + + return win; +} + +const spinnerWindowHtml = ` + + + + + +`; diff --git a/packages/thirdweb/src/react/wallets/embedded/socialIcons.ts b/packages/thirdweb/src/react/wallets/embedded/socialIcons.ts new file mode 100644 index 00000000000..5e35f161c15 --- /dev/null +++ b/packages/thirdweb/src/react/wallets/embedded/socialIcons.ts @@ -0,0 +1,11 @@ +import { + googleIconUri, + appleIconUri, + facebookIconUri, +} from "../../ui/ConnectWallet/icons/socialLogins.js"; + +export const socialIcons = { + google: googleIconUri, + apple: appleIconUri, + facebook: facebookIconUri, +}; diff --git a/packages/thirdweb/src/react/wallets/embedded/types.ts b/packages/thirdweb/src/react/wallets/embedded/types.ts new file mode 100644 index 00000000000..e6cb9faf068 --- /dev/null +++ b/packages/thirdweb/src/react/wallets/embedded/types.ts @@ -0,0 +1,18 @@ +import type { EmbeddedWallet } from "../../../wallets/embedded/core/wallet/index.js"; +import type { Account } from "../../../wallets/interfaces/wallet.js"; + +export type EmbeddedWalletSocialAuth = "google" | "apple" | "facebook"; + +export type EmbeddedWalletAuth = "email" | EmbeddedWalletSocialAuth; + +export type EmbeddedWalletSelectUIState = + | undefined + | { + emailLogin?: string; + // if a socialLogin is triggered, save the type and wallet instance that's in the process of being connected + socialLogin?: { + type: EmbeddedWalletSocialAuth; + wallet: EmbeddedWallet; + connectionPromise: Promise; + }; + }; diff --git a/packages/thirdweb/src/react/wallets/headlessConnectUI.tsx b/packages/thirdweb/src/react/wallets/headlessConnectUI.tsx index 4e944734c8a..5c43180bf39 100644 --- a/packages/thirdweb/src/react/wallets/headlessConnectUI.tsx +++ b/packages/thirdweb/src/react/wallets/headlessConnectUI.tsx @@ -12,7 +12,8 @@ import { Text } from "../ui/components/text.js"; * @internal */ export const HeadlessConnectUI = (props: ConnectUIProps) => { - const { walletConfig, screenConfig, done, createInstance } = props; + const { walletConfig, screenConfig } = props; + const { done, createInstance } = props.connection; const prompted = useRef(false); const [connectionFailed, setConnectionFailed] = useState(false); diff --git a/packages/thirdweb/src/react/wallets/shared/InjectedConnectUI.tsx b/packages/thirdweb/src/react/wallets/shared/InjectedConnectUI.tsx index 78fcca11f46..d6530ed3b0a 100644 --- a/packages/thirdweb/src/react/wallets/shared/InjectedConnectUI.tsx +++ b/packages/thirdweb/src/react/wallets/shared/InjectedConnectUI.tsx @@ -16,7 +16,8 @@ export const InjectedConnectUI = ( props.walletConfig.metadata.name, ); - const { walletConfig, done, screenConfig, createInstance } = props; + const { walletConfig, screenConfig } = props; + const { done, createInstance, chain } = props.connection; const [errorConnecting, setErrorConnecting] = useState(false); const connectToExtension = useCallback(async () => { @@ -26,14 +27,14 @@ export const InjectedConnectUI = ( await wait(1000); const wallet = createInstance(); await wallet.connect({ - chain: props.chain, + chain, }); done(wallet); } catch (e) { setErrorConnecting(true); console.error(e); } - }, [createInstance, done, props.chain]); + }, [createInstance, done, chain]); const connectPrompted = useRef(false); useEffect(() => { diff --git a/packages/thirdweb/src/react/wallets/shared/WalletConnectConnection.tsx b/packages/thirdweb/src/react/wallets/shared/WalletConnectConnection.tsx index 07c08aa1714..a7e8cef55ba 100644 --- a/packages/thirdweb/src/react/wallets/shared/WalletConnectConnection.tsx +++ b/packages/thirdweb/src/react/wallets/shared/WalletConnectConnection.tsx @@ -26,14 +26,14 @@ export const WalletConnectConnection: React.FC<{ }> = (props) => { const { onBack, onGetStarted, connectUIProps, projectId, platformUris } = props; - const { walletConfig, chain, done } = connectUIProps; + const { walletConfig } = connectUIProps; + const { chain, done, chains } = connectUIProps.connection; const locale = useTWLocale().wallets.injectedWallet( walletConfig.metadata.name, ); const { client, dappMetadata } = useThirdwebProviderProps(); const [qrCodeUri, setQrCodeUri] = useState(); const [errorConnecting, setErrorConnecting] = useState(false); - const optionalChains = connectUIProps.chains; const connect = useCallback(() => { const wallet = walletConnect({ @@ -72,7 +72,7 @@ export const WalletConnectConnection: React.FC<{ } }, onSessionRequestSent, - optionalChains, + optionalChains: chains, }) .then(() => { done(wallet); @@ -89,7 +89,7 @@ export const WalletConnectConnection: React.FC<{ platformUris, projectId, walletConfig.metadata, - optionalChains, + chains, ]); const scanStarted = useRef(false); diff --git a/packages/thirdweb/src/react/wallets/smartWallet/SmartWalletConnectUI.tsx b/packages/thirdweb/src/react/wallets/smartWallet/SmartWalletConnectUI.tsx index a761b9e77ca..2c929af8ad9 100644 --- a/packages/thirdweb/src/react/wallets/smartWallet/SmartWalletConnectUI.tsx +++ b/packages/thirdweb/src/react/wallets/smartWallet/SmartWalletConnectUI.tsx @@ -31,16 +31,19 @@ export const SmartConnectUI = (props: { const _props: ConnectUIProps = { walletConfig: personalWalletConfig, screenConfig: props.connectUIProps.screenConfig, - createInstance() { - return props.personalWalletConfig.create({ - client: client, - dappMetadata: dappMetadata, - }); + connection: { + createInstance() { + return props.personalWalletConfig.create({ + client: client, + dappMetadata: dappMetadata, + }); + }, + done(wallet) { + setPersonalWallet(wallet); + }, + chain: props.smartWalletChain, }, - done(wallet) { - setPersonalWallet(wallet); - }, - chain: props.smartWalletChain, + selection: props.connectUIProps.selection, }; if (personalWalletConfig.connectUI) { @@ -67,9 +70,10 @@ const SmartWalletConnecting = (props: { smartWalletChain: Chain; }) => { const locale = useTWLocale().wallets.smartWallet; - const createSmartWalletInstance = props.connectUIProps.createInstance; + const createSmartWalletInstance = + props.connectUIProps.connection.createInstance; const { personalWallet } = props; - const { done } = props.connectUIProps; + const { done } = props.connectUIProps.connection; const modalSize = props.connectUIProps.screenConfig.size; const [personalWalletChainId, setPersonalWalletChainId] = useState< diff --git a/packages/thirdweb/src/react/wallets/walletConnect/walletConnectConfig.tsx b/packages/thirdweb/src/react/wallets/walletConnect/walletConnectConfig.tsx index 66068703786..8bf4a1f4c87 100644 --- a/packages/thirdweb/src/react/wallets/walletConnect/walletConnectConfig.tsx +++ b/packages/thirdweb/src/react/wallets/walletConnect/walletConnectConfig.tsx @@ -80,7 +80,8 @@ const ConnectUI = ( ) => { const locale = useTWLocale().wallets.walletConnect; const [qrCodeUri, setQrCodeUri] = useState(); - const { chain, done, createInstance, walletConfig } = props; + const { walletConfig } = props; + const { chain, done, createInstance, chains } = props.connection; const [isWCModalOpen, setIsWCModalOpen] = useState(false); const { setModalVisibility } = props.screenConfig; @@ -99,7 +100,7 @@ const ConnectUI = ( chain: chain, showQrModal: true, qrModalOptions: props.wcConfig.qrModalOptions, - optionalChains: props.chains, + optionalChains: chains, }); done(wallet); @@ -167,15 +168,8 @@ function WalletConnectQRScanConnect( }; }, ) { - const { - qrCodeUri, - walletConfig, - createInstance, - done, - chain, - setQrCodeUri, - chains, - } = props; + const { qrCodeUri, walletConfig, setQrCodeUri } = props; + const { createInstance, done, chain, chains } = props.connection; const scanStarted = useRef(false); useEffect(() => { diff --git a/packages/thirdweb/src/wallets/embedded/core/wallet/index.ts b/packages/thirdweb/src/wallets/embedded/core/wallet/index.ts index 5533d92b643..3f8d0488cfe 100644 --- a/packages/thirdweb/src/wallets/embedded/core/wallet/index.ts +++ b/packages/thirdweb/src/wallets/embedded/core/wallet/index.ts @@ -14,6 +14,14 @@ import type { EmbeddedWalletConfig } from "./types.js"; import type { ThirdwebClient } from "../../../../client/client.js"; import type { Chain } from "../../../../chains/types.js"; import { ethereum } from "../../../../chains/chain-definitions/ethereum.js"; +import { + getSavedConnectParamsFromStorage, + saveConnectParamsToStorage, +} from "../../../manager/storage.js"; + +type SavedConnectParams = { + chain: Chain; +}; /** * Embedded Wallet @@ -36,21 +44,48 @@ export function embeddedWallet(args: EmbeddedWalletConfig) { return new EmbeddedWallet(args); } -class EmbeddedWallet implements Wallet { - metadata: WalletMetadata = { - id: "embedded-wallet", - name: "Embedded Wallet", - iconUrl: "", // TODO (ew) - }; +export const embeddedWalletMetadata: WalletMetadata = { + id: "embedded-wallet", + name: "Embedded Wallet", + iconUrl: + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzM1ODlfODY0OSkiPgo8cmVjdCB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHJ4PSI4IiBmaWxsPSJ1cmwoI3BhaW50MF9saW5lYXJfMzU4OV84NjQ5KSIvPgo8cmVjdCB4PSItMSIgeT0iLTEiIHdpZHRoPSI1MCIgaGVpZ2h0PSI1MCIgcng9IjkuOCIgZmlsbD0idXJsKCNwYWludDFfbGluZWFyXzM1ODlfODY0OSkiLz4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAxXzM1ODlfODY0OSkiPgo8cGF0aCBkPSJNMjQgMTQuMjVDMTguNjE3MiAxNC4yNSAxNC4yNSAxOC42MTcyIDE0LjI1IDI0QzE0LjI1IDI5LjM4MjggMTguNjE3MiAzMy43NSAyNCAzMy43NUMyNC44OTg4IDMzLjc1IDI1LjYyNSAzNC40NzYyIDI1LjYyNSAzNS4zNzVDMjUuNjI1IDM2LjI3MzggMjQuODk4OCAzNyAyNCAzN0MxNi44MTk1IDM3IDExIDMxLjE4MDUgMTEgMjRDMTEgMTYuODE5NSAxNi44MTk1IDExIDI0IDExQzMxLjE4MDUgMTEgMzcgMTYuODE5NSAzNyAyNFYyNS42MjVDMzcgMjguMzE2NCAzNC44MTY0IDMwLjUgMzIuMTI1IDMwLjVDMzAuNjM3MSAzMC41IDI5LjMwMTYgMjkuODI5NyAyOC40MDc4IDI4Ljc3ODVDMjcuMjUgMjkuODQ0OSAyNS43MDEyIDMwLjUgMjQgMzAuNUMyMC40MDk4IDMwLjUgMTcuNSAyNy41OTAyIDE3LjUgMjRDMTcuNSAyMC40MDk4IDIwLjQwOTggMTcuNSAyNCAxNy41QzI1LjQxNjggMTcuNSAyNi43MjcgMTcuOTUyIDI3Ljc5MzQgMTguNzIzOEMyOC4wODI4IDE4LjQ2OTkgMjguNDU4NiAxOC4zMTI1IDI4Ljg3NSAxOC4zMTI1QzI5Ljc3MzggMTguMzEyNSAzMC41IDE5LjAzODcgMzAuNSAxOS45Mzc1VjI1LjYyNUMzMC41IDI2LjUyMzggMzEuMjI2MiAyNy4yNSAzMi4xMjUgMjcuMjVDMzMuMDIzOCAyNy4yNSAzMy43NSAyNi41MjM4IDMzLjc1IDI1LjYyNVYyNEMzMy43NSAxOC42MTcyIDI5LjM4MjggMTQuMjUgMjQgMTQuMjVaTTI3LjI1IDI0QzI3LjI1IDIzLjEzOCAyNi45MDc2IDIyLjMxMTQgMjYuMjk4MSAyMS43MDE5QzI1LjY4ODYgMjEuMDkyNCAyNC44NjIgMjAuNzUgMjQgMjAuNzVDMjMuMTM4IDIwLjc1IDIyLjMxMTQgMjEuMDkyNCAyMS43MDE5IDIxLjcwMTlDMjEuMDkyNCAyMi4zMTE0IDIwLjc1IDIzLjEzOCAyMC43NSAyNEMyMC43NSAyNC44NjIgMjEuMDkyNCAyNS42ODg2IDIxLjcwMTkgMjYuMjk4MUMyMi4zMTE0IDI2LjkwNzYgMjMuMTM4IDI3LjI1IDI0IDI3LjI1QzI0Ljg2MiAyNy4yNSAyNS42ODg2IDI2LjkwNzYgMjYuMjk4MSAyNi4yOTgxQzI2LjkwNzYgMjUuNjg4NiAyNy4yNSAyNC44NjIgMjcuMjUgMjRaIiBmaWxsPSJ3aGl0ZSIvPgo8L2c+CjwvZz4KPGRlZnM+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQwX2xpbmVhcl8zNTg5Xzg2NDkiIHgxPSIyNS41IiB5MT0iLTYuMjk1NzJlLTA2IiB4Mj0iMzAuMjAxNiIgeTI9IjQ3LjUzNSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBzdG9wLWNvbG9yPSIjODM1OEJBIi8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzdCMUNGNyIvPgo8L2xpbmVhckdyYWRpZW50Pgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MV9saW5lYXJfMzU4OV84NjQ5IiB4MT0iMjUuNTYyNSIgeTE9Ii0xLjAwMDAxIiB4Mj0iMzAuNDYiIHkyPSI0OC41MTU2IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiM4MzU4QkEiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjN0IxQ0Y3Ii8+CjwvbGluZWFyR3JhZGllbnQ+CjxjbGlwUGF0aCBpZD0iY2xpcDBfMzU4OV84NjQ5Ij4KPHJlY3Qgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4IiByeD0iOCIgZmlsbD0id2hpdGUiLz4KPC9jbGlwUGF0aD4KPGNsaXBQYXRoIGlkPSJjbGlwMV8zNTg5Xzg2NDkiPgo8cmVjdCB3aWR0aD0iMjYiIGhlaWdodD0iMjYiIGZpbGw9IndoaXRlIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMSAxMSkiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K", // TODO (ew) +}; + +/** + * Embedded Wallet allows users to connect to connect using the Email or Social logins. + */ +export class EmbeddedWallet implements Wallet { + metadata: WalletMetadata; client: ThirdwebClient; account?: Account; - chain: Chain; + chain: Chain | undefined; + events: Wallet["events"]; - constructor(args: EmbeddedWalletConfig) { - this.client = args.client; - this.chain = args.defaultChain ?? ethereum; + /** + * Create a new Embedded Wallet + * @param options - The options to configure the embedded wallet initialization + * @example + * ```ts + * const wallet = new EmbeddedWallet({ + * client, + * }) + * ``` + */ + constructor(options: EmbeddedWalletConfig) { + this.client = options.client; + this.metadata = embeddedWalletMetadata; } + // is this used? + /** + * TODO + * @param options - The options for pre-authentication + * @example + * ```ts + * wallet.preAuthenticate(options); + * ``` + * @returns TODO + */ async preAuthenticate(options: Omit) { return preAuthenticate({ client: this.client, @@ -58,41 +93,110 @@ class EmbeddedWallet implements Wallet { }); } + /** + * Connect Embedded Wallet + * @param options - The options for configuring the connection + * @example + * ```ts + * const account = await wallet.connect(options); + * ``` + * @returns A Promise that resolves to the connected `Account` + */ async connect( - options: MultiStepAuthArgsType | SingleStepAuthArgsType, + options: (MultiStepAuthArgsType | SingleStepAuthArgsType) & { + chain?: Chain; + }, ): Promise { + this.chain = options.chain || ethereum; const authResult = await authenticate({ client: this.client, ...options, }); const authAccount = await authResult.user.wallet.getAccount(); this.account = authAccount; + + const params: SavedConnectParams = { + chain: this.chain, + }; + saveConnectParamsToStorage(this.metadata.id, params); + return authAccount; } + /** + * Auto connect to saved session + * @example + * ```ts + * const account = await wallet.autoConnect(); + * ``` + * @returns A Promise that resolves to the connected `Account` + */ async autoConnect(): Promise { const user = await getAuthenticatedUser({ client: this.client }); if (!user) { throw new Error("not authenticated"); } + + const savedParams: SavedConnectParams | null = + await getSavedConnectParamsFromStorage(this.metadata.id); + + if (savedParams) { + this.chain = savedParams.chain; + } else { + this.chain = ethereum; + } + const authAccount = await user.wallet.getAccount(); this.account = authAccount; return authAccount; } + /** + * Disconnect the wallet + * @example + * ```ts + * await wallet.disconnect(); + * ``` + */ async disconnect(): Promise { this.account = undefined; + this.chain = undefined; } + /** + * Get the account associated with the wallet + * @example + * ```ts + * const account = wallet.getAccount(); + * ``` + * @returns The `Account` object associated with the wallet + */ getAccount(): Account | undefined { return this.account; } + /** + * Get the chain the wallet is currently connected to + * @example + * ```ts + * const chain = wallet.getChain(); + * ``` + * @returns The `Chain` object for the chain the wallet is currently connected to + */ getChain() { return this.chain; } - async switchChain(newChain: Chain) { - this.chain = newChain; + /** + * Switch wallet to chain with given `Chain` object + * @param chain - The chain to switch to + * @example + * ```ts + * await wallet.switchChain(chain); + * ``` + */ + async switchChain(chain: Chain) { + saveConnectParamsToStorage(this.metadata.id, { chain }); + this.chain = chain; } } diff --git a/packages/thirdweb/src/wallets/embedded/core/wallet/types.ts b/packages/thirdweb/src/wallets/embedded/core/wallet/types.ts index d73167e1205..fb2d9dddf7d 100644 --- a/packages/thirdweb/src/wallets/embedded/core/wallet/types.ts +++ b/packages/thirdweb/src/wallets/embedded/core/wallet/types.ts @@ -1,7 +1,5 @@ -import type { Chain } from "../../../../chains/types.js"; import type { ThirdwebClient } from "../../../../client/client.js"; export type EmbeddedWalletConfig = { client: ThirdwebClient; - defaultChain?: Chain; }; diff --git a/packages/thirdweb/src/wallets/injected/index.ts b/packages/thirdweb/src/wallets/injected/index.ts index 3ae3656e2ce..8d80f704d7e 100644 --- a/packages/thirdweb/src/wallets/injected/index.ts +++ b/packages/thirdweb/src/wallets/injected/index.ts @@ -132,7 +132,7 @@ export class InjectedWallet implements Wallet { * ```ts * await wallet.autoConnect(); * ``` - * @returns A Promise that resolves to the connected address. + * @returns A Promise that resolves to the connected `Account` */ async autoConnect() { const provider = this.getProvider(); diff --git a/packages/thirdweb/tsdoc.json b/packages/thirdweb/tsdoc.json index 9d04eb47f84..ec805a59614 100644 --- a/packages/thirdweb/tsdoc.json +++ b/packages/thirdweb/tsdoc.json @@ -36,6 +36,10 @@ { "tagName": "@component", "syntaxKind": "block" + }, + { + "tagName": "@walletConfig", + "syntaxKind": "block" } ], "supportForTags": { @@ -47,6 +51,7 @@ "@connectWallet": true, "@theme": true, "@locale": true, - "@component": true + "@component": true, + "@walletConfig": true } }