diff --git a/examples/nextjs-bridge-mayan/package.json b/examples/nextjs-bridge-mayan/package.json index 6601949..9ebb8e0 100644 --- a/examples/nextjs-bridge-mayan/package.json +++ b/examples/nextjs-bridge-mayan/package.json @@ -9,9 +9,9 @@ "lint": "next lint" }, "dependencies": { - "@dynamic-labs-sdk/client": "1.2.1", - "@dynamic-labs-sdk/evm": "1.2.1", - "@dynamic-labs-sdk/react-hooks": "0.26.5", + "@dynamic-labs-sdk/client": "1.4.0", + "@dynamic-labs-sdk/evm": "1.4.0", + "@dynamic-labs-sdk/react-hooks": "1.4.0", "@mayanfinance/swap-sdk": "10.9.3", "lucide-react": "0.542.0", "@tanstack/react-query": "5.85.3", diff --git a/examples/nextjs-bridge-mayan/src/lib/dynamic.ts b/examples/nextjs-bridge-mayan/src/lib/dynamic.ts index 4947930..1fe5a5c 100644 --- a/examples/nextjs-bridge-mayan/src/lib/dynamic.ts +++ b/examples/nextjs-bridge-mayan/src/lib/dynamic.ts @@ -1,14 +1,25 @@ -import { createDynamicClient } from "@dynamic-labs-sdk/client"; -import { addEvmExtension } from "@dynamic-labs-sdk/evm"; +import { + createDynamicClient, + initializeClient, + type DynamicClient, +} from "@dynamic-labs-sdk/client"; +import { addWaasEvmExtension } from "@dynamic-labs-sdk/evm/waas"; -export const dynamicClient = createDynamicClient({ +export const dynamicClient: DynamicClient = createDynamicClient({ environmentId: process.env.NEXT_PUBLIC_DYNAMIC_ENV_ID!, + autoInitialize: false, metadata: { name: "Mayan Bridge" }, }); -if (typeof window !== "undefined") { - addEvmExtension(); -} +let initialized = false; -// No-op on clients that auto-initialize; called by useAuth on mount. -export async function initDynamic(): Promise {} +/** + * Adds the EVM WaaS extension and initializes the client. + * Safe to call multiple times — initialization runs once. + */ +export async function initDynamic(): Promise { + if (initialized) return; + initialized = true; + addWaasEvmExtension(dynamicClient); + await initializeClient(dynamicClient); +} diff --git a/examples/nextjs-bridge-mayan/src/lib/providers.tsx b/examples/nextjs-bridge-mayan/src/lib/providers.tsx index 2d5fbbc..15196a3 100644 --- a/examples/nextjs-bridge-mayan/src/lib/providers.tsx +++ b/examples/nextjs-bridge-mayan/src/lib/providers.tsx @@ -9,17 +9,16 @@ import { } from "react"; import { getWalletAccounts, - onEvent, isSignedIn, logout, - detectOAuthRedirect, - completeSocialAuthentication, + detectSocialRedirectUrl, + completeSocialRedirect, } from "@dynamic-labs-sdk/client"; import { createWaasWalletAccounts } from "@dynamic-labs-sdk/client/waas"; import { isEvmWalletAccount, type EvmWalletAccount } from "@dynamic-labs-sdk/evm"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { DynamicProvider, useUser, useWalletAccounts } from "@dynamic-labs-sdk/react-hooks"; -import { dynamicClient } from "./dynamic"; +import { DynamicProvider, useUser, useWalletAccounts, useEvent } from "@dynamic-labs-sdk/react-hooks"; +import { dynamicClient, initDynamic } from "./dynamic"; interface WalletContextValue { evmAccount: EvmWalletAccount | null; @@ -48,6 +47,24 @@ const queryClient = new QueryClient({ }, }); +function DynamicBootstrap() { + useEffect(() => { + let cancelled = false; + initDynamic().then(async () => { + if (cancelled || typeof window === "undefined") return; + try { + const url = new URL(window.location.href); + if (await detectSocialRedirectUrl({ url })) { + await completeSocialRedirect({ url }); + window.history.replaceState({}, "", window.location.pathname); + } + } catch { /* not a social redirect */ } + }); + return () => { cancelled = true; }; + }, []); + return null; +} + function InnerProviders({ children }: { children: ReactNode }) { const loggedIn = useUser() !== null; const evmAccount = useWalletAccounts().find(isEvmWalletAccount) ?? null; @@ -67,35 +84,7 @@ function InnerProviders({ children }: { children: ReactNode }) { } }, []); - useEffect(() => { - const unsub = onEvent( - { - event: "walletAccountsChanged", - listener: () => { - void ensureEvmWallet(); - }, - }, - dynamicClient, - ); - return () => unsub?.(); - }, [ensureEvmWallet]); - - useEffect(() => { - const handleOAuthRedirect = async () => { - if (typeof window === "undefined") return; - try { - const url = new URL(window.location.href); - if (await detectOAuthRedirect({ url }, dynamicClient)) { - await completeSocialAuthentication({ url }, dynamicClient); - await ensureEvmWallet(); - window.history.replaceState({}, "", window.location.pathname); - } - } catch { - // not an OAuth redirect - } - }; - handleOAuthRedirect(); - }, [ensureEvmWallet]); + useEvent({ event: "walletAccountsChanged", listener: () => { void ensureEvmWallet(); } }); return ( + {children} ); diff --git a/examples/nextjs-defi-lending-morpho/package.json b/examples/nextjs-defi-lending-morpho/package.json index 8f0a660..8357450 100644 --- a/examples/nextjs-defi-lending-morpho/package.json +++ b/examples/nextjs-defi-lending-morpho/package.json @@ -10,9 +10,9 @@ }, "dependencies": { "@coinbase/onchainkit": "0.38.17", - "@dynamic-labs-sdk/client": "1.2.1", - "@dynamic-labs-sdk/evm": "1.2.1", - "@dynamic-labs-sdk/react-hooks": "0.26.5", + "@dynamic-labs-sdk/client": "1.4.0", + "@dynamic-labs-sdk/evm": "1.4.0", + "@dynamic-labs-sdk/react-hooks": "1.4.0", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-slot": "1.2.3", diff --git a/examples/nextjs-defi-lending-morpho/src/lib/dynamic.ts b/examples/nextjs-defi-lending-morpho/src/lib/dynamic.ts index 2db9637..37effec 100644 --- a/examples/nextjs-defi-lending-morpho/src/lib/dynamic.ts +++ b/examples/nextjs-defi-lending-morpho/src/lib/dynamic.ts @@ -1,14 +1,25 @@ -import { createDynamicClient } from "@dynamic-labs-sdk/client"; -import { addEvmExtension } from "@dynamic-labs-sdk/evm"; +import { + createDynamicClient, + initializeClient, + type DynamicClient, +} from "@dynamic-labs-sdk/client"; +import { addWaasEvmExtension } from "@dynamic-labs-sdk/evm/waas"; -export const dynamicClient = createDynamicClient({ +export const dynamicClient: DynamicClient = createDynamicClient({ environmentId: process.env.NEXT_PUBLIC_DYNAMIC_ENV_ID!, + autoInitialize: false, metadata: { name: "Morpho Lending" }, }); -if (typeof window !== "undefined") { - addEvmExtension(); -} +let initialized = false; -// No-op on clients that auto-initialize; called by useAuth on mount. -export async function initDynamic(): Promise {} +/** + * Adds the EVM WaaS extension and initializes the client. + * Safe to call multiple times — initialization runs once. + */ +export async function initDynamic(): Promise { + if (initialized) return; + initialized = true; + addWaasEvmExtension(dynamicClient); + await initializeClient(dynamicClient); +} diff --git a/examples/nextjs-defi-lending-morpho/src/lib/providers.tsx b/examples/nextjs-defi-lending-morpho/src/lib/providers.tsx index fdf6807..2b18cde 100644 --- a/examples/nextjs-defi-lending-morpho/src/lib/providers.tsx +++ b/examples/nextjs-defi-lending-morpho/src/lib/providers.tsx @@ -10,18 +10,17 @@ import { } from "react"; import { getWalletAccounts, - onEvent, isSignedIn, logout, - detectOAuthRedirect, - completeSocialAuthentication, + detectSocialRedirectUrl, + completeSocialRedirect, getActiveNetworkId, } from "@dynamic-labs-sdk/client"; import { createWaasWalletAccounts } from "@dynamic-labs-sdk/client/waas"; import { isEvmWalletAccount, type EvmWalletAccount } from "@dynamic-labs-sdk/evm"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { DynamicProvider, useUser, useWalletAccounts } from "@dynamic-labs-sdk/react-hooks"; -import { dynamicClient } from "./dynamic"; +import { DynamicProvider, useUser, useWalletAccounts, useEvent } from "@dynamic-labs-sdk/react-hooks"; +import { dynamicClient, initDynamic } from "./dynamic"; interface WalletContextValue { evmAccount: EvmWalletAccount | null; @@ -54,6 +53,24 @@ const queryClient = new QueryClient({ }, }); +function DynamicBootstrap() { + useEffect(() => { + let cancelled = false; + initDynamic().then(async () => { + if (cancelled || typeof window === "undefined") return; + try { + const url = new URL(window.location.href); + if (await detectSocialRedirectUrl({ url })) { + await completeSocialRedirect({ url }); + window.history.replaceState({}, "", window.location.pathname); + } + } catch { /* not a social redirect */ } + }); + return () => { cancelled = true; }; + }, []); + return null; +} + function InnerProviders({ children }: { children: ReactNode }) { const loggedIn = useUser() !== null; const evmAccount = useWalletAccounts().find(isEvmWalletAccount) ?? null; @@ -79,33 +96,7 @@ function InnerProviders({ children }: { children: ReactNode }) { } catch {} }, []); - useEffect(() => { - const unsub = onEvent( - { - event: "walletAccountsChanged", - listener: () => { - void ensureEvmWallet(); - }, - }, - dynamicClient, - ); - return () => unsub?.(); - }, [ensureEvmWallet]); - - useEffect(() => { - const handleOAuthRedirect = async () => { - if (typeof window === "undefined") return; - try { - const url = new URL(window.location.href); - if (await detectOAuthRedirect({ url }, dynamicClient)) { - await completeSocialAuthentication({ url }, dynamicClient); - await ensureEvmWallet(); - window.history.replaceState({}, "", window.location.pathname); - } - } catch {} - }; - handleOAuthRedirect(); - }, [ensureEvmWallet]); + useEvent({ event: "walletAccountsChanged", listener: () => { void ensureEvmWallet(); } }); return ( + {children} ); diff --git a/examples/nextjs-delegated-access/components/dynamic/delegated-access/components/connect-wallet-prompt.tsx b/examples/nextjs-delegated-access/components/dynamic/delegated-access/components/connect-wallet-prompt.tsx index 3d510db..bc95378 100644 --- a/examples/nextjs-delegated-access/components/dynamic/delegated-access/components/connect-wallet-prompt.tsx +++ b/examples/nextjs-delegated-access/components/dynamic/delegated-access/components/connect-wallet-prompt.tsx @@ -1,20 +1,18 @@ "use client"; -import { DynamicEmbeddedWidget } from "@dynamic-labs/sdk-react-core"; +import DynamicButton from "@/components/dynamic/dynamic-widget"; /** - * Wallet connection prompt with embedded Dynamic widget + * Wallet connection prompt * - * Displayed when no wallet is connected. Uses Dynamic's embedded widget - * to provide a seamless authentication experience directly in the UI. - * - * The widget styling is configured via cssOverrides in lib/providers.tsx. + * Displayed when no wallet is connected. Uses the DynamicButton component + * to provide an authentication entry point directly in the UI. */ export default function ConnectWalletPrompt() { return (
- +
); diff --git a/examples/nextjs-delegated-access/components/dynamic/delegated-access/index.tsx b/examples/nextjs-delegated-access/components/dynamic/delegated-access/index.tsx index db27632..d230c15 100644 --- a/examples/nextjs-delegated-access/components/dynamic/delegated-access/index.tsx +++ b/examples/nextjs-delegated-access/components/dynamic/delegated-access/index.tsx @@ -14,12 +14,11 @@ "use client"; import { useState } from "react"; -import { Sparkles, Code2 } from "lucide-react"; -import { - SpinnerIcon, - useDynamicContext, - useWalletDelegation, -} from "@dynamic-labs/sdk-react-core"; +import { Sparkles, Code2, Loader2 } from "lucide-react"; +import { useUser, useWalletAccounts, useInitStatus } from "@dynamic-labs-sdk/react-hooks"; +import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm"; +import { hasDelegatedAccess } from "@dynamic-labs-sdk/client/waas"; +import { dynamicClient } from "@/lib/dynamic"; import DelegatedAccessInit from "./init"; import DelegatedAccessMethods from "./methods"; @@ -34,25 +33,43 @@ import DelegationInfoBox from "@/components/info/delegation-info-box"; type DelegationTab = "modal" | "custom"; export default function DelegatedAccess() { - const { sdkHasLoaded, primaryWallet } = useDynamicContext(); - const { - delegatedAccessEnabled, - getWalletsDelegatedStatus, - requiresDelegation, - } = useWalletDelegation(); + const user = useUser(); + const accounts = useWalletAccounts(); + const initStatus = useInitStatus(); + const sdkHasLoaded = initStatus === "finished"; + const primaryWallet = accounts.find(isEvmWalletAccount) ?? null; const [activeTab, setActiveTab] = useState("modal"); if (!sdkHasLoaded) { return (
- +
); } - const walletStatuses = getWalletsDelegatedStatus(); - const primaryWalletDelegationStatus = walletStatuses.find( - (wallet) => wallet.address === primaryWallet?.address, + // Derive wallet delegation statuses from the new SDK + const walletStatuses = accounts + .filter(isEvmWalletAccount) + .map((account) => { + let isDelegated = false; + try { + isDelegated = hasDelegatedAccess({ walletAccount: account }, dynamicClient); + } catch { + isDelegated = false; + } + return { + address: account.address, + status: isDelegated ? "delegated" : "pending", + }; + }); + + const primaryWalletDelegationStatus = primaryWallet + ? walletStatuses.find((wallet) => wallet.address === primaryWallet.address) + : undefined; + + const delegatedAccessEnabled = walletStatuses.some( + (wallet) => wallet.status === "delegated" ); return ( @@ -60,8 +77,8 @@ export default function DelegatedAccess() { {/* Main Status Card */}
diff --git a/examples/nextjs-delegated-access/components/dynamic/delegated-access/init.tsx b/examples/nextjs-delegated-access/components/dynamic/delegated-access/init.tsx index f42861e..61aefbb 100644 --- a/examples/nextjs-delegated-access/components/dynamic/delegated-access/init.tsx +++ b/examples/nextjs-delegated-access/components/dynamic/delegated-access/init.tsx @@ -3,20 +3,22 @@ import { useState } from "react"; import { Lock, Zap, Loader2, AlertCircle, Info } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { useDynamicContext, useWalletDelegation } from "@/lib/dynamic"; +import { useWalletAccounts } from "@dynamic-labs-sdk/react-hooks"; +import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm"; +import { delegateWaasKeyShares } from "@dynamic-labs-sdk/client/waas"; +import { dynamicClient } from "@/lib/dynamic"; /** - * DelegatedAccessInit - Delegation with Dynamic's Built-in Modal UI + * DelegatedAccessInit - Delegate the primary EVM wallet key share * - * This component demonstrates using initDelegationProcess() which: - * - Opens Dynamic's pre-built delegation modal - * - Handles the entire user flow automatically - * - Shows consent screens and progress indicators + * This component demonstrates using delegateWaasKeyShares() which: + * - Silently delegates the MPC key share to the server + * - Handles key generation and storage automatically * - Best for: Quick integration with minimal custom UI work */ export default function DelegatedAccessInit() { - const { primaryWallet } = useDynamicContext(); - const { initDelegationProcess } = useWalletDelegation(); + const accounts = useWalletAccounts(); + const primaryWallet = accounts.find(isEvmWalletAccount) ?? null; const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -30,7 +32,7 @@ export default function DelegatedAccessInit() { try { setIsLoading(true); setError(null); - await initDelegationProcess({ wallets: [primaryWallet] }); + await delegateWaasKeyShares({ walletAccount: primaryWallet }, dynamicClient); } catch (err) { const errorMessage = err instanceof Error ? err.message : "Delegation failed"; @@ -53,11 +55,11 @@ export default function DelegatedAccessInit() {

- initDelegationProcess() + delegateWaasKeyShares()

- Opens Dynamic's modal to guide users through delegation. - Handles consent, key generation, and success/error states automatically. + Delegates the MPC key share to the server programmatically. + Handles key generation and encrypted storage automatically.

@@ -65,12 +67,12 @@ export default function DelegatedAccessInit() { {/* What happens */}

- When triggered, the modal will: + When triggered, the delegation will:

- - - + + +
@@ -83,12 +85,12 @@ export default function DelegatedAccessInit() { {isLoading ? ( - Opening Modal... + Delegating... ) : ( - Open Delegation Modal + Delegate Key Share )} @@ -137,20 +139,18 @@ function CodeExample() {
-        {`const { initDelegationProcess } = useWalletDelegation();
+        {`import { delegateWaasKeyShares } from "@dynamic-labs-sdk/client/waas";
+import { dynamicClient } from "@/lib/dynamic";
 
-// Opens Dynamic's modal UI for delegation
-const handleDelegate = async () => {
+// Delegates the key share to the server
+const handleDelegate = async (walletAccount) => {
   try {
-    await initDelegationProcess();
+    await delegateWaasKeyShares({ walletAccount }, dynamicClient);
     console.log('Delegation completed!');
   } catch (error) {
-    console.error('User cancelled or error:', error);
+    console.error('Delegation failed:', error);
   }
-};
-
-// Or delegate specific wallets only
-await initDelegationProcess({ wallets: [primaryWallet] });`}
+};`}
       
); diff --git a/examples/nextjs-delegated-access/components/dynamic/delegated-access/management.tsx b/examples/nextjs-delegated-access/components/dynamic/delegated-access/management.tsx index a526198..271e591 100644 --- a/examples/nextjs-delegated-access/components/dynamic/delegated-access/management.tsx +++ b/examples/nextjs-delegated-access/components/dynamic/delegated-access/management.tsx @@ -11,31 +11,44 @@ import { Users, } from "lucide-react"; import { Button } from "@/components/ui/button"; +import { useWalletAccounts } from "@dynamic-labs-sdk/react-hooks"; +import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm"; import { - ChainEnum, - useDynamicContext, - useWalletDelegation, -} from "@/lib/dynamic"; + delegateWaasKeyShares, + hasDelegatedAccess, +} from "@dynamic-labs-sdk/client/waas"; +import { dynamicClient } from "@/lib/dynamic"; /** * DelegationManagement - Delegation with Custom UI (No Modal) * * This component demonstrates programmatic delegation without Dynamic's modal: - * - delegateKeyShares() - Direct delegation without showing any UI + * - delegateWaasKeyShares() - Direct delegation without showing any UI * - Best for: Custom delegation flows where you want full UI control */ export default function DelegationManagement() { - const { primaryWallet } = useDynamicContext(); - const { delegateKeyShares, getWalletsDelegatedStatus } = - useWalletDelegation(); + const accounts = useWalletAccounts(); + const evmAccounts = accounts.filter(isEvmWalletAccount); + const primaryWallet = evmAccounts[0] ?? null; const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); - // Get delegation status for all wallets from the SDK - // This is the source of truth for which wallets are eligible for delegation - const allWalletStatuses = getWalletsDelegatedStatus(); + // Derive delegation status for each EVM wallet account + const allWalletStatuses = evmAccounts.map((account) => { + let isDelegated = false; + try { + isDelegated = hasDelegatedAccess({ walletAccount: account }, dynamicClient); + } catch { + isDelegated = false; + } + return { + account, + address: account.address, + status: isDelegated ? "delegated" : "pending", + }; + }); // Wallets with status "pending" are eligible for delegation const pendingWallets = allWalletStatuses.filter( @@ -70,12 +83,10 @@ export default function DelegationManagement() { setError(null); setSuccess(null); - await delegateKeyShares([ - { - chainName: primaryWalletStatus.chain as ChainEnum, - accountAddress: primaryWalletStatus.address, - }, - ]); + await delegateWaasKeyShares( + { walletAccount: primaryWallet }, + dynamicClient + ); setSuccess("Primary wallet delegated successfully!"); } catch (err) { @@ -102,12 +113,12 @@ export default function DelegationManagement() { setError(null); setSuccess(null); - const walletsToDelegate = pendingWallets.map((wallet) => ({ - chainName: wallet.chain as ChainEnum, - accountAddress: wallet.address, - })); - - await delegateKeyShares(walletsToDelegate); + for (const wallet of pendingWallets) { + await delegateWaasKeyShares( + { walletAccount: wallet.account }, + dynamicClient + ); + } setSuccess(`${pendingWallets.length} wallet(s) delegated successfully!`); } catch (err) { @@ -136,7 +147,7 @@ export default function DelegationManagement() {
-

delegateKeyShares()

+

delegateWaasKeyShares()

Delegates key shares programmatically without any UI. Build your own consent flow and call this when ready. @@ -299,22 +310,18 @@ function CodeExample() {

-        {`const { delegateKeyShares } = useWalletDelegation();
+        {`import { delegateWaasKeyShares } from "@dynamic-labs-sdk/client/waas";
+import { dynamicClient } from "@/lib/dynamic";
 
 // Delegate without showing any Dynamic UI
-const handleDelegate = async () => {
+const handleDelegate = async (walletAccount) => {
   try {
-    await delegateKeyShares([
-      { chainName: ChainEnum.Evm, accountAddress: '0x...' }
-    ]);
+    await delegateWaasKeyShares({ walletAccount }, dynamicClient);
     console.log('Delegation completed!');
   } catch (error) {
     console.error('Delegation failed:', error);
   }
-};
-
-// Or delegate all pending wallets at once
-await delegateKeyShares();`}
+};`}
       
); diff --git a/examples/nextjs-delegated-access/components/dynamic/delegated-access/methods.tsx b/examples/nextjs-delegated-access/components/dynamic/delegated-access/methods.tsx index 20851cd..e722295 100644 --- a/examples/nextjs-delegated-access/components/dynamic/delegated-access/methods.tsx +++ b/examples/nextjs-delegated-access/components/dynamic/delegated-access/methods.tsx @@ -10,11 +10,10 @@ import { Loader2, } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { - ChainEnum, - useDynamicContext, - useWalletDelegation, -} from "@/lib/dynamic"; +import { useUser, useWalletAccounts } from "@dynamic-labs-sdk/react-hooks"; +import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm"; +import { revokeWaasDelegation } from "@dynamic-labs-sdk/client/waas"; +import { dynamicClient } from "@/lib/dynamic"; import { authFetch } from "@/lib/dynamic/auth-fetch"; import { EcdsaKeygenResult } from "@dynamic-labs-wallet/node"; import ResponseDisplay from "./components/response-display"; @@ -43,8 +42,9 @@ interface SignMessageResponse { type ActionType = "getKey" | "sign" | "revoke" | null; export default function DelegatedAccessMethods() { - const { user, primaryWallet } = useDynamicContext(); - const { revokeDelegation } = useWalletDelegation(); + const user = useUser(); + const accounts = useWalletAccounts(); + const primaryWallet = accounts.find(isEvmWalletAccount) ?? null; const [result, setResult] = useState(""); const [error, setError] = useState(null); @@ -59,16 +59,15 @@ export default function DelegatedAccessMethods() { setLastAction(null); } - async function handleRevokeDelegation(address: string) { + async function handleRevokeDelegation() { + if (!primaryWallet) return; try { setIsLoading(true); setActiveAction("revoke"); setError(null); setResult(""); - await revokeDelegation([ - { accountAddress: address, chainName: ChainEnum.Evm }, - ]); + await revokeWaasDelegation({ walletAccount: primaryWallet }, dynamicClient); setResult("Delegation revoked successfully"); setLastAction("revoke"); @@ -283,7 +282,7 @@ export default function DelegatedAccessMethods() { - {primaryWallet && isEthereumWallet(primaryWallet) && ( + {primaryWallet && (
Wallet Methods diff --git a/examples/nextjs-delegated-access/components/dynamic/dynamic-widget.tsx b/examples/nextjs-delegated-access/components/dynamic/dynamic-widget.tsx index 1e63150..5fa01f7 100644 --- a/examples/nextjs-delegated-access/components/dynamic/dynamic-widget.tsx +++ b/examples/nextjs-delegated-access/components/dynamic/dynamic-widget.tsx @@ -1,7 +1,373 @@ "use client"; -import { DynamicWidget as DynamicWidgetComponent } from "@/lib/dynamic"; +import { useState, useRef, useEffect } from "react"; +import { + useUser, + useWalletAccounts, + useInitStatus, +} from "@dynamic-labs-sdk/react-hooks"; +import { + signInWithSocialRedirect, + logout, + sendEmailOTP, + verifyOTP, + type OTPVerification, +} from "@dynamic-labs-sdk/client"; +import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm"; +import { Check, ChevronLeft, Copy, Loader2, Mail } from "lucide-react"; +import { dynamicClient } from "@/lib/dynamic"; -export default function DynamicWidget() { - return ; +type AuthStep = "menu" | "email" | "otp"; + +function truncate(addr: string) { + if (!addr || addr.length < 10) return addr; + return `${addr.slice(0, 6)}…${addr.slice(-4)}`; +} + +function truncateEmail(email: string, max = 22) { + if (email.length <= max) return email; + const [local, domain] = email.split("@"); + if (!domain) return `${email.slice(0, max - 1)}…`; + const keep = Math.max(3, max - domain.length - 2); + return `${local.slice(0, keep)}…@${domain}`; +} + +function AddressRow({ + label, + addr, + copied, + onCopy, +}: { + label: string; + addr: string; + copied: boolean; + onCopy: () => void; +}) { + return ( +
+
+

+ {label} +

+

{truncate(addr)}

+
+ +
+ ); +} + +export default function DynamicButton() { + const user = useUser(); + const accounts = useWalletAccounts(); + const initStatus = useInitStatus(); + const loggedIn = user !== null; + + const [open, setOpen] = useState(false); + const [step, setStep] = useState("menu"); + const [email, setEmail] = useState(""); + const [otp, setOtp] = useState(""); + const [otpVerification, setOtpVerification] = + useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [copied, setCopied] = useState(null); + const ref = useRef(null); + + useEffect(() => { + function onClick(e: MouseEvent) { + if (ref.current && !ref.current.contains(e.target as Node)) { + setOpen(false); + setStep("menu"); + setError(null); + } + } + document.addEventListener("mousedown", onClick); + return () => document.removeEventListener("mousedown", onClick); + }, []); + + const evm = accounts.find(isEvmWalletAccount); + + const resetAuth = () => { + setEmail(""); + setOtp(""); + setOtpVerification(null); + }; + + const copy = (addr: string, key: string) => { + navigator.clipboard.writeText(addr); + setCopied(key); + setTimeout(() => setCopied(null), 2000); + }; + + const handleGoogle = async () => { + setLoading(true); + setError(null); + try { + await signInWithSocialRedirect( + { provider: "google", redirectUrl: globalThis.location.origin }, + dynamicClient + ); + } catch (e) { + setError(e instanceof Error ? e.message : "Google sign-in failed."); + } finally { + setLoading(false); + } + }; + + const handleEmail = async () => { + if (!email) return; + setLoading(true); + setError(null); + try { + setOtpVerification(await sendEmailOTP({ email }, dynamicClient)); + setStep("otp"); + } catch { + setError("Failed to send code. Please try again."); + } finally { + setLoading(false); + } + }; + + const handleVerify = async () => { + if (!otp || !otpVerification) return; + setLoading(true); + setError(null); + try { + await verifyOTP( + { otpVerification, verificationToken: otp }, + dynamicClient + ); + setOpen(false); + setStep("menu"); + resetAuth(); + } catch { + setError("Invalid code. Please try again."); + } finally { + setLoading(false); + } + }; + + // Initializing: gate the button until the SDK is ready + if (initStatus !== "finished") { + return ( + + ); + } + + // Signed in: avatar + address → dropdown menu + if (loggedIn) { + const primary = evm?.address ?? ""; + const userEmail = user?.email ?? ""; + const label = userEmail ? truncateEmail(userEmail) : truncate(primary); + const addrStart = primary.startsWith("0x") ? 2 : 0; + const initials = ( + userEmail ? userEmail.slice(0, 2) : primary.slice(addrStart, 2) + ).toUpperCase(); + return ( +
+ + {open && ( +
+
+

+ Signed in as +

+

+ {user?.email ?? "—"} +

+
+
+ {accounts.length === 0 ? ( +
+ + Setting up your wallet… +
+ ) : ( + <> + {evm && ( + copy(evm.address, "evm")} + /> + )} + + )} +
+ +
+ )} +
+ ); + } + + // Signed out: Sign in → dropdown (Google / email → OTP) + return ( +
+ + + {open && ( +
+ {error && ( +

{error}

+ )} + + {step === "menu" && ( + <> + + + + )} + + {step === "email" && ( + <> + +

+ Enter your email +

+ setEmail(e.target.value)} + placeholder="you@example.com" + className="w-full px-3 py-2 text-sm rounded-lg border border-border bg-background text-foreground outline-none focus:ring-2 focus:ring-ring/30" + onKeyDown={(e) => e.key === "Enter" && handleEmail()} + /> + + + )} + + {step === "otp" && ( + <> + +

+ Code sent to{" "} + {email} +

+ + setOtp(e.target.value.replace(/\D/g, "").slice(0, 6)) + } + placeholder="Enter 6-digit code" + className="w-full px-3 py-2 text-sm rounded-lg border border-border bg-background text-foreground text-center tracking-widest outline-none focus:ring-2 focus:ring-ring/30" + onKeyDown={(e) => e.key === "Enter" && handleVerify()} + /> + + + )} +
+ )} +
+ ); } diff --git a/examples/nextjs-delegated-access/components/dynamic/logout-button.tsx b/examples/nextjs-delegated-access/components/dynamic/logout-button.tsx index 361d81f..48b4bdb 100644 --- a/examples/nextjs-delegated-access/components/dynamic/logout-button.tsx +++ b/examples/nextjs-delegated-access/components/dynamic/logout-button.tsx @@ -1,7 +1,9 @@ "use client"; -import { useDynamicContext } from "@dynamic-labs/sdk-react-core"; +import { useUser } from "@dynamic-labs-sdk/react-hooks"; +import { logout } from "@dynamic-labs-sdk/client"; import { Button } from "@/components/ui/button"; +import { dynamicClient } from "@/lib/dynamic"; /** * Logout button for the navigation header @@ -11,14 +13,14 @@ import { Button } from "@/components/ui/button"; * to keep the Header component as a server component. */ export default function LogoutButton() { - const { user, handleLogOut } = useDynamicContext(); + const user = useUser(); if (!user) return null; return ( - - {/* Shows the currently active (primary) wallet with network selector */} + {/* Shows the currently active embedded wallet */} - {/* Shows all wallets linked to this account */} + {/* Shows all embedded wallet accounts */} )} diff --git a/examples/nextjs-external-wallets/components/dynamic/dynamic-active-wallet.tsx b/examples/nextjs-external-wallets/components/dynamic/dynamic-active-wallet.tsx index 3e77911..d8dd914 100644 --- a/examples/nextjs-external-wallets/components/dynamic/dynamic-active-wallet.tsx +++ b/examples/nextjs-external-wallets/components/dynamic/dynamic-active-wallet.tsx @@ -3,161 +3,43 @@ /** * DynamicActiveWallet * - * Displays the currently active (primary) wallet with: - * - Wallet icon and name + * Displays the currently active embedded wallet with: + * - Wallet type label (EVM or Solana) * - Truncated address - * - Network selector (for Ethereum wallets that support network switching) - * - Static network badge (for Solana or wallets that don't support switching) + * - Chain badge + * + * The new headless JS SDK uses embedded (MPC/WaaS) wallets — connector-based + * network switching is not available, so this component shows a static badge. */ -import { isEthereumWallet } from "@dynamic-labs/ethereum"; -import { useDynamicContext } from "@dynamic-labs/sdk-react-core"; -import { useWalletBookCdn, WalletIcon } from "@dynamic-labs/wallet-book"; -import { useEffect, useState } from "react"; -import { fetchWalletNetwork, type NetworkInfo } from "@/lib/get-wallet-network"; -import { truncateAddress } from "@/lib/truncate-address"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "../ui/select"; +import { useWalletAccounts } from "@dynamic-labs-sdk/react-hooks"; +import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm"; +import { isSolanaWalletAccount } from "@dynamic-labs-sdk/solana"; export default function DynamicActiveWallet() { - // Get the primary (currently active) wallet from Dynamic context - const { primaryWallet } = useDynamicContext(); - - // Get wallet metadata (icons, names) from the wallet book - const { wallets: walletBookWallets } = useWalletBookCdn(); - - // Network state for display - const [network, setNetwork] = useState({ name: "Unknown" }); - const [currentNetworkId, setCurrentNetworkId] = useState< - string | number | null - >(null); - - /** - * Handles network switching when user selects a different network. - * Only applicable for Ethereum wallets that support network switching. - */ - const handleNetworkChange = async (value: string) => { - if (!primaryWallet || !isEthereumWallet(primaryWallet)) return; - - const chainId = parseInt(value, 10); - - if (primaryWallet.connector.supportsNetworkSwitching()) { - try { - // Request the wallet to switch networks - await primaryWallet.switchNetwork(chainId); - setCurrentNetworkId(chainId); - - // Update the displayed network info immediately - const enabledNetworks = primaryWallet.connector.getEnabledNetworks(); - const networkInfo = enabledNetworks?.find( - (n) => String(n.chainId || n.networkId) === String(chainId) - ); - - if (networkInfo) { - setNetwork({ - name: networkInfo.vanityName || networkInfo.name || "Unknown", - iconUrl: networkInfo.iconUrls?.[0], - }); - } - } catch (error) { - console.error("Error switching network", error); - } - } - }; + const accounts = useWalletAccounts(); + const evmAccount = accounts.find(isEvmWalletAccount); + const solanaAccount = accounts.find(isSolanaWalletAccount); - // Fetch network info when the primary wallet changes - useEffect(() => { - const loadNetwork = async () => { - try { - const result = await fetchWalletNetwork(primaryWallet); - setNetwork(result.network); - setCurrentNetworkId(result.networkId); - } catch (error) { - console.debug("Error fetching network info:", error); - setNetwork({ name: "Unknown" }); - setCurrentNetworkId(null); - } - }; - loadNetwork(); - }, [primaryWallet]); - - // Don't render if no wallet is connected - if (!primaryWallet) return null; - - // Normalize wallet key for wallet book lookup - // (Solana wallets have "sol" suffix that needs to be removed) - const walletKey = primaryWallet.key.endsWith("sol") - ? primaryWallet.key.slice(0, -3) - : primaryWallet.key; - - const walletName = walletBookWallets[walletKey]?.name || walletKey; + const primaryAccount = evmAccount ?? solanaAccount; + if (!primaryAccount) return null; return (
- {/* Wallet Icon */} -
- -
- {/* Wallet Info */}
-
{walletName}
-
- {truncateAddress(primaryWallet.address)} +
+ {evmAccount ? "EVM Wallet" : "Solana Wallet"} +
+
+ {primaryAccount.address.slice(0, 6)}...{primaryAccount.address.slice(-4)}
- {/* Network Selector (Ethereum) or Static Badge (Solana/other) */} - {isEthereumWallet(primaryWallet) && - primaryWallet.connector.supportsNetworkSwitching() ? ( - // Interactive network selector for Ethereum wallets - - ) : ( - // Static network badge for Solana or wallets without network switching -
- {network.iconUrl && ( -
- )} - - {network.name} - -
- )} + {/* Static chain badge */} + + {evmAccount ? "EVM" : "Solana"} +
); } diff --git a/examples/nextjs-external-wallets/components/dynamic/dynamic-auth-button.tsx b/examples/nextjs-external-wallets/components/dynamic/dynamic-auth-button.tsx index 882c996..9d2efeb 100644 --- a/examples/nextjs-external-wallets/components/dynamic/dynamic-auth-button.tsx +++ b/examples/nextjs-external-wallets/components/dynamic/dynamic-auth-button.tsx @@ -4,45 +4,387 @@ * DynamicAuthButton * * A login/logout button that: - * - Shows "Login" when not authenticated (opens Dynamic auth flow) - * - Shows "Logout" when authenticated - * - Handles hydration by showing a disabled state during SSR + * - Shows a loading state while the SDK initializes + * - Shows "Log in or sign up" when not authenticated (Google + Email OTP) + * - Shows the user's address/email with a dropdown when authenticated */ -import { useDynamicContext, useIsLoggedIn } from "@dynamic-labs/sdk-react-core"; -import { useEffect, useState } from "react"; -import { Button } from "../ui/button"; +import { useState, useRef, useEffect } from "react"; +import { + useUser, + useWalletAccounts, + useInitStatus, +} from "@dynamic-labs-sdk/react-hooks"; +import { + signInWithSocialRedirect, + logout, + sendEmailOTP, + verifyOTP, + type OTPVerification, +} from "@dynamic-labs-sdk/client"; +import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm"; +import { isSolanaWalletAccount } from "@dynamic-labs-sdk/solana"; +import { Check, ChevronLeft, Copy, Loader2, Mail } from "lucide-react"; +import { dynamicClient } from "@/lib/dynamic"; + +type AuthStep = "menu" | "email" | "otp"; + +function truncate(addr: string) { + if (!addr || addr.length < 10) return addr; + return `${addr.slice(0, 6)}…${addr.slice(-4)}`; +} + +function truncateEmail(email: string, max = 22) { + if (email.length <= max) return email; + const [local, domain] = email.split("@"); + if (!domain) return `${email.slice(0, max - 1)}…`; + const keep = Math.max(3, max - domain.length - 2); + return `${local.slice(0, keep)}…@${domain}`; +} + +function AddressRow({ + label, + addr, + copied, + onCopy, +}: { + label: string; + addr: string; + copied: boolean; + onCopy: () => void; +}) { + return ( +
+
+

+ {label} +

+

{truncate(addr)}

+
+ +
+ ); +} export default function DynamicAuthButton() { - // Dynamic SDK hooks for auth control - const { setShowAuthFlow, handleLogOut } = useDynamicContext(); - const isLoggedIn = useIsLoggedIn(); + const user = useUser(); + const accounts = useWalletAccounts(); + const initStatus = useInitStatus(); + const loggedIn = user !== null; - // Track client-side mount to prevent hydration mismatch - const [mounted, setMounted] = useState(false); + const [open, setOpen] = useState(false); + const [step, setStep] = useState("menu"); + const [email, setEmail] = useState(""); + const [otp, setOtp] = useState(""); + const [otpVerification, setOtpVerification] = useState( + null + ); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [copied, setCopied] = useState(null); + const ref = useRef(null); useEffect(() => { - setMounted(true); + function onClick(e: MouseEvent) { + if (ref.current && !ref.current.contains(e.target as Node)) { + setOpen(false); + setStep("menu"); + setError(null); + } + } + document.addEventListener("mousedown", onClick); + return () => document.removeEventListener("mousedown", onClick); }, []); - // Show a neutral disabled state during SSR to match server render - if (!mounted) { + const evm = accounts.find(isEvmWalletAccount); + const solana = accounts.find(isSolanaWalletAccount); + + const resetAuth = () => { + setEmail(""); + setOtp(""); + setOtpVerification(null); + }; + + const copy = (addr: string, key: string) => { + navigator.clipboard.writeText(addr); + setCopied(key); + setTimeout(() => setCopied(null), 2000); + }; + + const handleGoogle = async () => { + setLoading(true); + setError(null); + try { + await signInWithSocialRedirect( + { provider: "google", redirectUrl: globalThis.location.origin }, + dynamicClient + ); + } catch (e) { + setError(e instanceof Error ? e.message : "Google sign-in failed."); + } finally { + setLoading(false); + } + }; + + const handleEmail = async () => { + if (!email) return; + setLoading(true); + setError(null); + try { + setOtpVerification(await sendEmailOTP({ email }, dynamicClient)); + setStep("otp"); + } catch { + setError("Failed to send code. Please try again."); + } finally { + setLoading(false); + } + }; + + const handleVerify = async () => { + if (!otp || !otpVerification) return; + setLoading(true); + setError(null); + try { + await verifyOTP({ otpVerification, verificationToken: otp }, dynamicClient); + setOpen(false); + setStep("menu"); + resetAuth(); + } catch { + setError("Invalid code. Please try again."); + } finally { + setLoading(false); + } + }; + + // Initializing: gate the button until the SDK is ready + if (initStatus !== "finished") { return ( - + ); } - // Show logout button when authenticated - if (isLoggedIn) { + // Signed in: avatar + address → dropdown menu + if (loggedIn) { + const primary = evm?.address ?? solana?.address ?? ""; + const userEmail = user?.email ?? ""; + const label = userEmail ? truncateEmail(userEmail) : truncate(primary); + const addrStart = primary.startsWith("0x") ? 2 : 0; + const initials = ( + userEmail ? userEmail.slice(0, 2) : primary.slice(addrStart, 2) + ).toUpperCase(); return ( - +
+ + {open && ( +
+
+

+ Signed in as +

+

+ {user?.email ?? "—"} +

+
+
+ {accounts.length === 0 ? ( +
+ + Setting up your wallets… +
+ ) : ( + <> + {evm && ( + copy(evm.address, "evm")} + /> + )} + {solana && ( + copy(solana.address, "sol")} + /> + )} + + )} +
+ +
+ )} +
); } - // Show login button when not authenticated - return ; + // Signed out: Sign in → dropdown (Google / email → OTP) + return ( +
+ + + {open && ( +
+ {error && ( +

{error}

+ )} + + {step === "menu" && ( + <> + + + + )} + + {step === "email" && ( + <> + +

+ Enter your email +

+ setEmail(e.target.value)} + placeholder="you@example.com" + className="w-full px-3 py-2 text-sm rounded-lg border border-border bg-background text-foreground outline-none focus:ring-2 focus:ring-ring/30" + onKeyDown={(e) => e.key === "Enter" && handleEmail()} + /> + + + )} + + {step === "otp" && ( + <> + +

+ Code sent to{" "} + {email} +

+ + setOtp(e.target.value.replace(/\D/g, "").slice(0, 6)) + } + placeholder="Enter 6-digit code" + className="w-full px-3 py-2 text-sm rounded-lg border border-border bg-background text-foreground text-center tracking-widest outline-none focus:ring-2 focus:ring-ring/30" + onKeyDown={(e) => e.key === "Enter" && handleVerify()} + /> + + + )} +
+ )} +
+ ); } diff --git a/examples/nextjs-external-wallets/components/dynamic/dynamic-methods.tsx b/examples/nextjs-external-wallets/components/dynamic/dynamic-methods.tsx index 1612f60..4e880b0 100644 --- a/examples/nextjs-external-wallets/components/dynamic/dynamic-methods.tsx +++ b/examples/nextjs-external-wallets/components/dynamic/dynamic-methods.tsx @@ -8,30 +8,26 @@ * * Available methods: * - Fetch User: Shows the current user object - * - Fetch User Wallets: Shows all wallets linked to the user - * - Fetch PublicClient: Gets viem PublicClient (Ethereum only) - * - Fetch WalletClient: Gets viem WalletClient (Ethereum only) - * - Sign Message: Signs a test message with the wallet (Ethereum only) + * - Fetch Wallet Accounts: Shows all embedded wallet accounts + * - Sign Message: Signs a test message with the EVM embedded wallet */ -import { isEthereumWallet } from "@dynamic-labs/ethereum"; import { Check, Copy } from "lucide-react"; import { redirect } from "next/navigation"; import { useEffect, useState } from "react"; -import { - useDynamicContext, - useIsLoggedIn, - useUserWallets, -} from "@/lib/dynamic"; +import { useUser, useWalletAccounts, useInitStatus } from "@dynamic-labs-sdk/react-hooks"; +import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm"; import DynamicWidget from "./dynamic-widget"; import { Button } from "../ui/button"; import { Skeleton } from "../ui/skeleton"; export default function DynamicMethods() { // Dynamic SDK hooks - const isLoggedIn = useIsLoggedIn(); - const { sdkHasLoaded, primaryWallet, user } = useDynamicContext(); - const userWallets = useUserWallets(); + const user = useUser(); + const accounts = useWalletAccounts(); + const initStatus = useInitStatus(); + const isLoggedIn = user !== null; + const evmAccount = accounts.find(isEvmWalletAccount); // UI state const [isLoading, setIsLoading] = useState(true); @@ -41,12 +37,20 @@ export default function DynamicMethods() { // Redirect to home if not logged in (after SDK loads) useEffect(() => { - if (sdkHasLoaded && !isLoggedIn) redirect("/"); - }, [sdkHasLoaded, isLoggedIn]); + if (initStatus === "finished" && !isLoggedIn) redirect("/"); + }, [initStatus, isLoggedIn]); + + // Update loading state based on SDK readiness + useEffect(() => { + if (initStatus === "finished" && isLoggedIn) { + setIsLoading(false); + } else { + setIsLoading(true); + } + }, [initStatus, isLoggedIn]); /** * Safely stringifies objects, handling circular references. - * Dynamic SDK objects often have circular refs that break JSON.stringify. */ const safeStringify = (obj: unknown): string => { const seen = new WeakSet(); @@ -63,15 +67,6 @@ export default function DynamicMethods() { ); }; - // Update loading state based on SDK readiness - useEffect(() => { - if (sdkHasLoaded && isLoggedIn && primaryWallet) { - setIsLoading(false); - } else { - setIsLoading(true); - } - }, [sdkHasLoaded, isLoggedIn, primaryWallet]); - /** Clears the result panel */ function clearResult() { setResult(""); @@ -90,10 +85,10 @@ export default function DynamicMethods() { } } - /** Displays all wallets linked to the user */ - function showUserWallets() { + /** Displays all embedded wallet accounts linked to the user */ + function showWalletAccounts() { try { - setResult(safeStringify(userWallets)); + setResult(safeStringify(accounts)); setError(null); } catch (err) { setError( @@ -102,49 +97,15 @@ export default function DynamicMethods() { } } - /** Fetches the viem PublicClient from the Ethereum wallet */ - async function fetchEthereumPublicClient() { - if (!primaryWallet || !isEthereumWallet(primaryWallet)) return; + /** Signs a test message using the EVM embedded wallet */ + async function signEvmMessage() { + if (!evmAccount) return; try { setIsLoading(true); - const client = await primaryWallet.getPublicClient(); - setResult(safeStringify(client)); - } catch (err) { - setResult( - safeStringify({ - error: err instanceof Error ? err.message : "Unknown error occurred", - }) - ); - } finally { - setIsLoading(false); - } - } - - /** Fetches the viem WalletClient from the Ethereum wallet */ - async function fetchEthereumWalletClient() { - if (!primaryWallet || !isEthereumWallet(primaryWallet)) return; - try { - setIsLoading(true); - const client = await primaryWallet.getWalletClient(); - setResult(safeStringify(client)); - } catch (err) { - setResult( - safeStringify({ - error: err instanceof Error ? err.message : "Unknown error occurred", - }) - ); - } finally { - setIsLoading(false); - } - } - - /** Signs a test message using the Ethereum wallet */ - async function signEthereumMessage() { - if (!primaryWallet || !isEthereumWallet(primaryWallet)) return; - try { - setIsLoading(true); - const signature = await primaryWallet.signMessage("Hello World"); + const { signMessage } = await import("@dynamic-labs-sdk/client"); + const { signature } = await signMessage({ walletAccount: evmAccount, message: "Hello World" }); setResult(safeStringify(signature)); + setError(null); } catch (err) { setResult( safeStringify({ @@ -211,7 +172,7 @@ export default function DynamicMethods() { {/* Methods Panel */}
- {/* Dynamic Widget or Loading State */} + {/* Auth widget or loading state */} {!isLoading ? ( ) : ( @@ -229,35 +190,21 @@ export default function DynamicMethods() { - {/* Ethereum-specific Methods */} - {primaryWallet && isEthereumWallet(primaryWallet) && ( + {/* EVM Wallet Methods */} + {evmAccount && (
- Ethereum Wallet Methods + EVM Wallet Methods
- - - - {/* Delete Button */} - +
); } diff --git a/examples/nextjs-external-wallets/components/dynamic/dynamic-wallet-list.tsx b/examples/nextjs-external-wallets/components/dynamic/dynamic-wallet-list.tsx index d7a6c2e..dd6bfc1 100644 --- a/examples/nextjs-external-wallets/components/dynamic/dynamic-wallet-list.tsx +++ b/examples/nextjs-external-wallets/components/dynamic/dynamic-wallet-list.tsx @@ -3,42 +3,62 @@ /** * DynamicWalletList * - * Displays all wallets linked to the current user's account. - * Each wallet can be clicked to make it the primary wallet, - * or deleted to remove it from the account. + * Displays all embedded wallet accounts linked to the current user. + * Shows EVM and Solana embedded wallets created by the WaaS SDK. */ -import { useUserWallets } from "@dynamic-labs/sdk-react-core"; -import DynamicWalletItem from "./dynamic-wallet-item"; +import { useWalletAccounts } from "@dynamic-labs-sdk/react-hooks"; +import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm"; +import { isSolanaWalletAccount } from "@dynamic-labs-sdk/solana"; export default function DynamicWalletList() { - // Get all wallets linked to the current user - const wallets = useUserWallets(); + const accounts = useWalletAccounts(); + const evmAccount = accounts.find(isEvmWalletAccount); + const solanaAccount = accounts.find(isSolanaWalletAccount); return (
{/* Header with wallet count */}
-

- Connected Wallets -

- {wallets.length > 0 && ( - - {wallets.length} {wallets.length === 1 ? "wallet" : "wallets"} - - )} +

Embedded Wallets

+ + {accounts.length} {accounts.length === 1 ? "wallet" : "wallets"} +
{/* Wallet list or empty state */} - {wallets.length === 0 ? ( + {accounts.length === 0 ? (
- No wallets connected + No wallets found
) : (
- {wallets.map((wallet) => ( - - ))} + {evmAccount && ( +
+
+
EVM Wallet
+
+ {evmAccount.address.slice(0, 6)}...{evmAccount.address.slice(-4)} +
+
+ + EVM + +
+ )} + {solanaAccount && ( +
+
+
Solana Wallet
+
+ {solanaAccount.address.slice(0, 6)}...{solanaAccount.address.slice(-4)} +
+
+ + Solana + +
+ )}
)}
diff --git a/examples/nextjs-external-wallets/components/dynamic/dynamic-widget.tsx b/examples/nextjs-external-wallets/components/dynamic/dynamic-widget.tsx index 2042ca2..ef9e675 100644 --- a/examples/nextjs-external-wallets/components/dynamic/dynamic-widget.tsx +++ b/examples/nextjs-external-wallets/components/dynamic/dynamic-widget.tsx @@ -3,12 +3,12 @@ /** * DynamicWidget * - * Wrapper component for Dynamic's built-in widget. - * Used on the Methods page to show the standard Dynamic UI. + * Auth widget used on the Methods page to show the user's auth state. + * Renders the DynamicAuthButton which handles login/logout with the new SDK. */ -import { DynamicWidget as DynamicWidgetComponent } from "@/lib/dynamic"; +import DynamicAuthButton from "./dynamic-auth-button"; export default function DynamicWidget() { - return ; + return ; } diff --git a/examples/nextjs-external-wallets/lib/dynamic.ts b/examples/nextjs-external-wallets/lib/dynamic.ts index 6ada218..e0da164 100644 --- a/examples/nextjs-external-wallets/lib/dynamic.ts +++ b/examples/nextjs-external-wallets/lib/dynamic.ts @@ -1,11 +1,27 @@ +import { + createDynamicClient, + initializeClient, + type DynamicClient, +} from "@dynamic-labs-sdk/client"; +import { addWaasEvmExtension } from "@dynamic-labs-sdk/evm/waas"; +import { addWaasSolanaExtension } from "@dynamic-labs-sdk/solana/waas"; + +export const dynamicClient: DynamicClient = createDynamicClient({ + environmentId: process.env.NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID!, + autoInitialize: false, + metadata: { name: "External Wallets Demo" }, +}); + +let initialized = false; + /** - * Dynamic SDK Re-exports - * - * This file centralizes all Dynamic SDK imports to make it easier to manage - * and swap out imports across the application. It re-exports everything from - * the Ethereum, Solana, and core SDK packages. + * Adds the EVM + Solana WaaS extensions and initializes the client. + * Safe to call multiple times — initialization runs once. */ - -export * from "@dynamic-labs/ethereum"; -export * from "@dynamic-labs/sdk-react-core"; -export * from "@dynamic-labs/solana"; +export async function initDynamic(): Promise { + if (initialized) return; + initialized = true; + addWaasEvmExtension(dynamicClient); + addWaasSolanaExtension(dynamicClient); + await initializeClient(dynamicClient); +} diff --git a/examples/nextjs-external-wallets/lib/get-wallet-network.ts b/examples/nextjs-external-wallets/lib/get-wallet-network.ts index 231bedf..9921546 100644 --- a/examples/nextjs-external-wallets/lib/get-wallet-network.ts +++ b/examples/nextjs-external-wallets/lib/get-wallet-network.ts @@ -1,14 +1,12 @@ /** * Wallet Network Utilities * - * Helper functions for fetching network information from connected wallets. - * Supports both Ethereum and Solana wallets. + * Note: This file is retained for reference but is no longer used. + * The new headless JS SDK uses embedded (MPC/WaaS) wallets which do not + * expose connector-based network info in the same way as the old React SDK. + * Network information is instead managed by the SDK client directly. */ -import { isEthereumWallet } from "@dynamic-labs/ethereum"; -import type { Wallet } from "@dynamic-labs/sdk-react-core"; -import { isSolanaWallet } from "@dynamic-labs/solana"; - /** Network display information */ export interface NetworkInfo { name: string; @@ -22,80 +20,13 @@ export interface NetworkResult { } /** Default Solana network info */ -const SOLANA_NETWORK: NetworkInfo = { +export const SOLANA_NETWORK: NetworkInfo = { name: "Solana", iconUrl: "https://app.dynamic.xyz/assets/networks/solana.svg", }; -/** Fallback for unknown networks */ -const UNKNOWN_NETWORK: NetworkInfo = { name: "Unknown" }; - -/** - * Fetches network information for a given wallet. - * - * - For Solana wallets: Returns static Solana network info - * - For Ethereum wallets: Queries the connector for the current network - * - For other wallets: Returns unknown network - * - * @param wallet - The wallet to fetch network info for - * @returns Network information including name, icon URL, and network ID - */ -export async function fetchWalletNetwork( - wallet: Wallet | null -): Promise { - // Handle null wallet - if (!wallet) { - return { network: UNKNOWN_NETWORK, networkId: null }; - } - - // Solana wallets don't have multiple networks in the same way Ethereum does - if (isSolanaWallet(wallet)) { - return { network: SOLANA_NETWORK, networkId: null }; - } - - // Only Ethereum wallets have network switching capabilities - if (!isEthereumWallet(wallet)) { - return { network: UNKNOWN_NETWORK, networkId: null }; - } - - // Get the Ethereum connector to query network info - const connector = wallet.connector; - if (!connector?.getEnabledNetworks) { - return { network: UNKNOWN_NETWORK, networkId: null }; - } - - // Get list of networks enabled in the Dynamic dashboard - const enabledNetworks = connector.getEnabledNetworks(); - if (!enabledNetworks || enabledNetworks.length === 0) { - return { network: UNKNOWN_NETWORK, networkId: null }; - } - - // Try to get the wallet's current network - let networkId: string | number | undefined; - try { - networkId = await connector.getNetwork(); - } catch { - // If we can't get the current network, use the first enabled network as fallback - const defaultNetwork = enabledNetworks[0]; - return { - network: { - name: defaultNetwork.vanityName || defaultNetwork.name || "Unknown", - iconUrl: defaultNetwork.iconUrls?.[0], - }, - networkId: null, - }; - } - - // Find the network info matching the current network ID - const networkInfo = enabledNetworks.find( - (n) => String(n.chainId || n.networkId) === String(networkId) - ); - - return { - network: { - name: networkInfo?.vanityName || networkInfo?.name || "Unknown", - iconUrl: networkInfo?.iconUrls?.[0], - }, - networkId: networkId ?? null, - }; -} +/** Default EVM network info */ +export const EVM_NETWORK: NetworkInfo = { + name: "Ethereum", + iconUrl: "https://app.dynamic.xyz/assets/networks/eth.svg", +}; diff --git a/examples/nextjs-external-wallets/lib/providers.tsx b/examples/nextjs-external-wallets/lib/providers.tsx index d51bce0..ec8b0af 100644 --- a/examples/nextjs-external-wallets/lib/providers.tsx +++ b/examples/nextjs-external-wallets/lib/providers.tsx @@ -1,30 +1,83 @@ "use client"; +import { dynamicClient, initDynamic } from "@/lib/dynamic"; import { - useWalletBookCdn, - WalletBookContextProvider, -} from "@dynamic-labs/wallet-book"; -import { ThemeProvider } from "@/components/theme-provider"; + completeSocialRedirect, + detectSocialRedirectUrl, +} from "@dynamic-labs-sdk/client"; import { - DynamicContextProvider, - DynamicMultiWalletPromptsWidget, - EthereumWalletConnectors, - SolanaWalletConnectors, -} from "@/lib/dynamic"; + createWaasWalletAccounts, + getChainsMissingWaasWalletAccounts, +} from "@dynamic-labs-sdk/client/waas"; +import { DynamicProvider, useEvent } from "@dynamic-labs-sdk/react-hooks"; +import { useEffect } from "react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ThemeProvider } from "@/components/theme-provider"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + refetchOnWindowFocus: false, + }, + }, +}); + +/** + * Initializes the client, then completes the Google OAuth redirect (returns + * with ?dynamicOauthCode=…) so the user is hydrated after social sign-in. + */ +function DynamicBootstrap() { + useEffect(() => { + let cancelled = false; + initDynamic().then(async () => { + if (cancelled || typeof globalThis.window === "undefined") return; + try { + const url = new URL(globalThis.location.href); + if (await detectSocialRedirectUrl({ url })) { + await completeSocialRedirect({ url }); + globalThis.history.replaceState({}, "", globalThis.location.pathname); + } + } catch { + /* not a social redirect */ + } + }); + return () => { + cancelled = true; + }; + }, []); + return null; +} + +/** + * Once a user signs in, create embedded wallets for any enabled chain that's + * missing one — using getChainsMissingWaasWalletAccounts() rather than an + * accounts.length check, which can be momentarily stale right after auth. + */ +function WalletBootstrap() { + useEvent({ + event: "userChanged", + listener: async (user) => { + if (!user) return; + const missing = getChainsMissingWaasWalletAccounts(dynamicClient); + if (missing.length > 0) { + await createWaasWalletAccounts({ chains: missing }, dynamicClient); + } + }, + }); + return null; +} /** * Application Providers * * Wraps the application with all necessary context providers: * - ThemeProvider: Handles light/dark mode theming - * - DynamicContextProvider: Core Dynamic SDK provider for wallet authentication - * - WalletBookContextProvider: Provides wallet metadata (icons, names) from CDN - * - DynamicMultiWalletPromptsWidget: Handles multi-wallet prompts and modals + * - DynamicProvider: Core Dynamic SDK provider for wallet authentication + * - DynamicBootstrap: Initializes client and handles social OAuth redirects + * - WalletBootstrap: Auto-creates embedded wallets after sign-in */ export default function Providers({ children }: { children: React.ReactNode }) { - // Fetch wallet icons and metadata from Dynamic's CDN - const walletBook = useWalletBookCdn(); - return ( - - {/* Provides wallet icons and display names for the UI */} - + + + + {children} - - - {/* Renders prompts for multi-wallet actions (linking, switching, etc.) */} - - + + ); } diff --git a/examples/nextjs-external-wallets/package.json b/examples/nextjs-external-wallets/package.json index d9eefa4..2b6d21d 100644 --- a/examples/nextjs-external-wallets/package.json +++ b/examples/nextjs-external-wallets/package.json @@ -8,10 +8,11 @@ "start": "next start" }, "dependencies": { - "@dynamic-labs/ethereum": "4.49.0", - "@dynamic-labs/sdk-react-core": "4.49.0", - "@dynamic-labs/solana": "4.49.0", - "@dynamic-labs/wallet-book": "4.50.4", + "@dynamic-labs-sdk/client": "1.4.0", + "@dynamic-labs-sdk/evm": "1.4.0", + "@dynamic-labs-sdk/react-hooks": "1.4.0", + "@dynamic-labs-sdk/solana": "1.4.0", + "@tanstack/react-query": "5.100.14", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-select": "2.2.6", "@radix-ui/react-slot": "1.2.4", diff --git a/examples/nextjs-gasless-relayer/.env.example b/examples/nextjs-gasless-relayer/.env.example index bbc0223..113f469 100644 --- a/examples/nextjs-gasless-relayer/.env.example +++ b/examples/nextjs-gasless-relayer/.env.example @@ -1,6 +1,6 @@ # Dynamic SDK # Get your Dynamic environment ID from the Dynamic Dashboard: https://app.dynamic.xyz -NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID=your-dynamic-environment-id +NEXT_PUBLIC_DYNAMIC_ENV_ID=your-dynamic-environment-id # Solana RPC (optional, defaults to mainnet) NEXT_PUBLIC_SOLANA_RPC_URL=https://api.mainnet-beta.solana.com diff --git a/examples/nextjs-gasless-relayer/app/page.tsx b/examples/nextjs-gasless-relayer/app/page.tsx index 543643e..2a7b150 100644 --- a/examples/nextjs-gasless-relayer/app/page.tsx +++ b/examples/nextjs-gasless-relayer/app/page.tsx @@ -1,11 +1,12 @@ "use client"; -import { useIsLoggedIn } from "@dynamic-labs/sdk-react-core"; -import { DynamicWidget } from "@dynamic-labs/sdk-react-core"; +import { useUser } from "@dynamic-labs-sdk/react-hooks"; +import DynamicButton from "@/components/dynamic-button"; import GaslessTransactionDemo from "@/components/gasless-transaction-demo"; export default function Home() { - const isLoggedIn = useIsLoggedIn(); + const user = useUser(); + const isLoggedIn = user !== null; return (
@@ -15,7 +16,7 @@ export default function Home() {
- +
{isLoggedIn && ( @@ -33,4 +34,3 @@ export default function Home() { ); } - diff --git a/examples/nextjs-gasless-relayer/components/dynamic-button.tsx b/examples/nextjs-gasless-relayer/components/dynamic-button.tsx new file mode 100644 index 0000000..4d94279 --- /dev/null +++ b/examples/nextjs-gasless-relayer/components/dynamic-button.tsx @@ -0,0 +1,364 @@ +"use client"; + +import { useState, useRef, useEffect } from "react"; +import { useUser, useWalletAccounts, useInitStatus } from "@dynamic-labs-sdk/react-hooks"; +import { + signInWithSocialRedirect, + logout, + sendEmailOTP, + verifyOTP, + type OTPVerification, +} from "@dynamic-labs-sdk/client"; +import { isSolanaWalletAccount } from "@dynamic-labs-sdk/solana"; +import { dynamicClient } from "@/lib/dynamic"; + +type AuthStep = "menu" | "email" | "otp"; + +function truncate(addr: string) { + if (!addr || addr.length < 10) return addr; + return `${addr.slice(0, 6)}…${addr.slice(-4)}`; +} + +function truncateEmail(email: string, max = 22) { + if (email.length <= max) return email; + const [local, domain] = email.split("@"); + if (!domain) return `${email.slice(0, max - 1)}…`; + const keep = Math.max(3, max - domain.length - 2); + return `${local.slice(0, keep)}…@${domain}`; +} + +function AddressRow({ + label, + addr, + copied, + onCopy, +}: { + label: string; + addr: string; + copied: boolean; + onCopy: () => void; +}) { + return ( +
+
+

{label}

+

{truncate(addr)}

+
+ +
+ ); +} + +export default function DynamicButton() { + const user = useUser(); + const accounts = useWalletAccounts(); + const initStatus = useInitStatus(); + const loggedIn = user !== null; + + const [open, setOpen] = useState(false); + const [step, setStep] = useState("menu"); + const [email, setEmail] = useState(""); + const [otp, setOtp] = useState(""); + const [otpVerification, setOtpVerification] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [copied, setCopied] = useState(null); + const ref = useRef(null); + + useEffect(() => { + function onClick(e: MouseEvent) { + if (ref.current && !ref.current.contains(e.target as Node)) { + setOpen(false); + setStep("menu"); + setError(null); + } + } + document.addEventListener("mousedown", onClick); + return () => document.removeEventListener("mousedown", onClick); + }, []); + + const solanaWallet = accounts.find(isSolanaWalletAccount); + + const resetAuth = () => { + setEmail(""); + setOtp(""); + setOtpVerification(null); + }; + + const copy = (addr: string, key: string) => { + navigator.clipboard.writeText(addr); + setCopied(key); + setTimeout(() => setCopied(null), 2000); + }; + + const handleGoogle = async () => { + setLoading(true); + setError(null); + try { + await signInWithSocialRedirect( + { provider: "google", redirectUrl: globalThis.location.origin }, + dynamicClient + ); + } catch (e) { + setError(e instanceof Error ? e.message : "Google sign-in failed."); + } finally { + setLoading(false); + } + }; + + const handleEmail = async () => { + if (!email) return; + setLoading(true); + setError(null); + try { + setOtpVerification(await sendEmailOTP({ email }, dynamicClient)); + setStep("otp"); + } catch { + setError("Failed to send code. Please try again."); + } finally { + setLoading(false); + } + }; + + const handleVerify = async () => { + if (!otp || !otpVerification) return; + setLoading(true); + setError(null); + try { + await verifyOTP({ otpVerification, verificationToken: otp }, dynamicClient); + setOpen(false); + setStep("menu"); + resetAuth(); + } catch { + setError("Invalid code. Please try again."); + } finally { + setLoading(false); + } + }; + + if (initStatus !== "finished") { + return ( + + ); + } + + if (loggedIn) { + const primary = solanaWallet?.address ?? ""; + const userEmail = user?.email ?? ""; + const label = userEmail ? truncateEmail(userEmail) : truncate(primary); + const initials = (userEmail ? userEmail.slice(0, 2) : primary.slice(0, 2)).toUpperCase(); + + return ( +
+ + {open && ( +
+
+

Signed in as

+

{user?.email ?? "—"}

+
+
+ {accounts.length === 0 ? ( +
+ + + + + Setting up your wallets... +
+ ) : ( + solanaWallet && ( + copy(solanaWallet.address, "sol")} + /> + ) + )} +
+ +
+ )} +
+ ); + } + + return ( +
+ + + {open && ( +
+ {error && ( +

{error}

+ )} + + {step === "menu" && ( + <> + + + + )} + + {step === "email" && ( + <> + +

Enter your email

+ setEmail(e.target.value)} + placeholder="you@example.com" + className="w-full px-3 py-2 text-sm rounded-lg border border-gray-300 bg-white text-gray-900 outline-none focus:ring-2 focus:ring-blue-300" + onKeyDown={(e) => e.key === "Enter" && handleEmail()} + /> + + + )} + + {step === "otp" && ( + <> + +

+ Code sent to {email} +

+ setOtp(e.target.value.replace(/\D/g, "").slice(0, 6))} + placeholder="Enter 6-digit code" + className="w-full px-3 py-2 text-sm rounded-lg border border-gray-300 bg-white text-gray-900 text-center tracking-widest outline-none focus:ring-2 focus:ring-blue-300" + onKeyDown={(e) => e.key === "Enter" && handleVerify()} + /> + + + )} +
+ )} +
+ ); +} diff --git a/examples/nextjs-gasless-relayer/components/gasless-transaction-demo.tsx b/examples/nextjs-gasless-relayer/components/gasless-transaction-demo.tsx index 1222c2c..37d13b4 100644 --- a/examples/nextjs-gasless-relayer/components/gasless-transaction-demo.tsx +++ b/examples/nextjs-gasless-relayer/components/gasless-transaction-demo.tsx @@ -1,7 +1,8 @@ "use client"; -import { useDynamicContext, useIsLoggedIn } from "@dynamic-labs/sdk-react-core"; -import { isSolanaWallet } from "@dynamic-labs/solana"; +import { useUser, useWalletAccounts } from "@dynamic-labs-sdk/react-hooks"; +import { isSolanaWalletAccount, signTransaction } from "@dynamic-labs-sdk/solana"; +import { dynamicClient } from "@/lib/dynamic"; import { updateOrAppendSetComputeUnitLimitInstruction, updateOrAppendSetComputeUnitPriceInstruction, @@ -45,8 +46,10 @@ const CONFIG = { }; export default function GaslessTransactionDemo() { - const isLoggedIn = useIsLoggedIn(); - const { primaryWallet } = useDynamicContext(); + const user = useUser(); + const isLoggedIn = user !== null; + const accounts = useWalletAccounts(); + const solanaWallet = accounts.find(isSolanaWalletAccount) ?? null; const [status, setStatus] = useState(""); const [loading, setLoading] = useState(false); const [transactionSignature, setTransactionSignature] = useState< @@ -57,7 +60,7 @@ export default function GaslessTransactionDemo() { useEffect(() => { const fetchTokenBalance = async () => { - if (!primaryWallet || !isSolanaWallet(primaryWallet)) { + if (!solanaWallet) { setTokenBalance(null); return; } @@ -66,7 +69,7 @@ export default function GaslessTransactionDemo() { try { const rpc = createSolanaRpc(CONFIG.solanaRpcUrl); const mintAddress = address(CONFIG.tokenMintAddress); - const ownerAddress = address(primaryWallet.address); + const ownerAddress = address(solanaWallet.address); const [ata] = await findAssociatedTokenPda({ mint: mintAddress, @@ -111,10 +114,10 @@ export default function GaslessTransactionDemo() { }; fetchTokenBalance(); - }, [primaryWallet]); + }, [solanaWallet]); const handleGaslessTransaction = async () => { - if (!primaryWallet || !isSolanaWallet(primaryWallet)) { + if (!solanaWallet) { setStatus("Error: Solana wallet not available or not properly connected"); return; } @@ -190,7 +193,7 @@ export default function GaslessTransactionDemo() { const initialPaymentResponse = await koraClient.getPaymentInstruction({ transaction: initialEstimateBase64, fee_token: paymentToken, - source_wallet: primaryWallet.address, + source_wallet: solanaWallet.address, }); let paymentInstruction: Instruction = initialPaymentResponse.payment_instruction; @@ -236,7 +239,7 @@ export default function GaslessTransactionDemo() { const finalPaymentResponse = await koraClient.getPaymentInstruction({ transaction: finalEstimateBase64, fee_token: paymentToken, - source_wallet: primaryWallet.address, + source_wallet: solanaWallet.address, }); paymentInstruction = finalPaymentResponse.payment_instruction; @@ -288,7 +291,7 @@ export default function GaslessTransactionDemo() { const message = originalTransaction.message; const numRequiredSignatures = message.header.numRequiredSignatures; const accountKeys = message.staticAccountKeys; - const userAddress = primaryWallet.address; + const userAddress = solanaWallet.address; let userSignatureIndex = -1; for ( @@ -308,9 +311,9 @@ export default function GaslessTransactionDemo() { ); } - const signer = await primaryWallet.getSigner(); - const signedTransaction = await signer.signTransaction( - originalTransaction as any + const signedTransaction = await signTransaction( + { transaction: originalTransaction as any, walletAccount: solanaWallet }, + dynamicClient ); const userSignature = signedTransaction.signatures[userSignatureIndex]; @@ -387,9 +390,7 @@ export default function GaslessTransactionDemo() {
Wallet:{" "} - {primaryWallet && isSolanaWallet(primaryWallet) - ? primaryWallet.address - : "Not connected"} + {solanaWallet ? solanaWallet.address : "Not connected"}
Kora RPC: {CONFIG.koraRpcUrl} @@ -397,7 +398,7 @@ export default function GaslessTransactionDemo() {
Solana RPC: {CONFIG.solanaRpcUrl}
- {primaryWallet && isSolanaWallet(primaryWallet) && ( + {solanaWallet && (
Token Balance ({CONFIG.tokenMintAddress.slice(0, 8)}...): @@ -415,12 +416,7 @@ export default function GaslessTransactionDemo() { + ); + } if (isLoggedIn) { + const address = evmWallet?.address; + const shortAddress = address ? `${address.slice(0, 6)}...${address.slice(-4)}` : "No wallet"; return ( - +
+ + {showDropdown && ( +
+ +
+ )} +
); } + + async function handleGoogleLogin() { + setLoading(true); + setError(null); + try { + await signInWithSocialRedirect({ provider: "google" }, dynamicClient); + } catch { + setError("Google login failed"); + setLoading(false); + } + } + + async function handleSendOTP() { + if (!email) return; + setLoading(true); + setError(null); + try { + const verification = await sendEmailOTP({ email }, dynamicClient); + setOtpVerification(verification); + setShowOtpInput(true); + setShowEmailInput(false); + } catch { + setError("Failed to send OTP"); + } finally { + setLoading(false); + } + } + + async function handleVerifyOTP() { + if (!otpVerification || !otp) return; + setLoading(true); + setError(null); + try { + await verifyOTP({ otp }, otpVerification); + setShowDropdown(false); + setShowOtpInput(false); + } catch { + setError("Invalid OTP"); + } finally { + setLoading(false); + } + } + return ( - - Log in or sign up - +
+ + {showDropdown && !showEmailInput && !showOtpInput && ( +
+ + + {error &&

{error}

} +
+ )} + {showDropdown && showEmailInput && ( +
+

Enter your email

+ setEmail(e.target.value)} + placeholder="you@example.com" + className="w-full px-3 py-2 text-sm border rounded" + onKeyDown={(e) => e.key === "Enter" && handleSendOTP()} + /> + + {error &&

{error}

} +
+ )} + {showDropdown && showOtpInput && ( +
+

Enter OTP sent to {email}

+ setOtp(e.target.value)} + placeholder="123456" + className="w-full px-3 py-2 text-sm border rounded" + onKeyDown={(e) => e.key === "Enter" && handleVerifyOTP()} + /> + + {error &&

{error}

} +
+ )} +
); } diff --git a/examples/nextjs-gateway-arc/src/components/header.tsx b/examples/nextjs-gateway-arc/src/components/header.tsx index e6f38de..e8687bf 100644 --- a/examples/nextjs-gateway-arc/src/components/header.tsx +++ b/examples/nextjs-gateway-arc/src/components/header.tsx @@ -1,5 +1,4 @@ import Link from "next/link"; -import { DynamicWidget } from "@dynamic-labs/sdk-react-core"; import DynamicLogo from "./dynamic/logo"; import { HamburgerMenu } from "./hamburger-menu"; import DynamicButton from "./dynamic/dynamic-button"; @@ -15,7 +14,7 @@ export default function Header() {
- +
diff --git a/examples/nextjs-gateway-arc/src/lib/dynamic.ts b/examples/nextjs-gateway-arc/src/lib/dynamic.ts index fa58ecd..65c8df5 100644 --- a/examples/nextjs-gateway-arc/src/lib/dynamic.ts +++ b/examples/nextjs-gateway-arc/src/lib/dynamic.ts @@ -1,8 +1,17 @@ -export * from "@dynamic-labs/sdk-react-core"; -export * from "@dynamic-labs/ethereum"; -export * from "@dynamic-labs/solana"; -export { - ZeroDevSmartWalletConnectors, - isZeroDevConnector, -} from "@dynamic-labs/ethereum-aa"; -export { DynamicWagmiConnector } from "@dynamic-labs/wagmi-connector"; +import { createDynamicClient, initializeClient, type DynamicClient } from "@dynamic-labs-sdk/client"; +import { addWaasEvmExtension } from "@dynamic-labs-sdk/evm/waas"; + +export const dynamicClient: DynamicClient = createDynamicClient({ + environmentId: process.env.NEXT_PUBLIC_DYNAMIC_ENV_ID!, + autoInitialize: false, + metadata: { name: "Circle Gateway with Dynamic" }, +}); + +let initialized = false; + +export async function initDynamic(): Promise { + if (initialized) return; + initialized = true; + addWaasEvmExtension(dynamicClient); + await initializeClient(dynamicClient); +} diff --git a/examples/nextjs-gateway-arc/src/lib/providers.tsx b/examples/nextjs-gateway-arc/src/lib/providers.tsx index 2db8e7b..2aaa3e0 100644 --- a/examples/nextjs-gateway-arc/src/lib/providers.tsx +++ b/examples/nextjs-gateway-arc/src/lib/providers.tsx @@ -1,19 +1,52 @@ "use client"; -import { config } from "@/lib/wagmi"; +import { useEffect } from "react"; +import { DynamicProvider, useEvent } from "@dynamic-labs-sdk/react-hooks"; +import { completeSocialRedirect, detectSocialRedirectUrl } from "@dynamic-labs-sdk/client"; +import { createWaasWalletAccounts, getChainsMissingWaasWalletAccounts } from "@dynamic-labs-sdk/client/waas"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { WagmiProvider } from "wagmi"; import { ThemeProvider } from "@/components/theme-provider"; -import { - DynamicContextProvider, - EthereumWalletConnectors, - ZeroDevSmartWalletConnectors, - DynamicWagmiConnector, -} from "@/lib/dynamic"; +import { dynamicClient, initDynamic } from "./dynamic"; -export default function Providers({ children }: { children: React.ReactNode }) { - const queryClient = new QueryClient(); +const queryClient = new QueryClient({ + defaultOptions: { queries: { staleTime: 1000 * 60 * 5, refetchOnWindowFocus: false } }, +}); + +function DynamicBootstrap() { + useEffect(() => { + let cancelled = false; + initDynamic().then(async () => { + if (cancelled || typeof window === "undefined") return; + try { + const url = new URL(window.location.href); + if (await detectSocialRedirectUrl({ url })) { + await completeSocialRedirect({ url }); + window.history.replaceState({}, "", window.location.pathname); + } + } catch {} + }); + return () => { + cancelled = true; + }; + }, []); + return null; +} +function WalletBootstrap() { + useEvent({ + event: "userChanged", + listener: async (user) => { + if (!user) return; + const missing = getChainsMissingWaasWalletAccounts(dynamicClient); + if (missing.length > 0) { + await createWaasWalletAccounts({ chains: missing }, dynamicClient); + } + }, + }); + return null; +} + +export default function Providers({ children }: { children: React.ReactNode }) { return ( - - - - {children} - - - + + + + + {children} + + ); } diff --git a/examples/nextjs-gateway-arc/src/lib/wagmi.ts b/examples/nextjs-gateway-arc/src/lib/wagmi.ts index 3a03d37..96ccd2c 100644 --- a/examples/nextjs-gateway-arc/src/lib/wagmi.ts +++ b/examples/nextjs-gateway-arc/src/lib/wagmi.ts @@ -1,21 +1,2 @@ -import { createConfig, http } from "wagmi"; -import { arcTestnet, baseSepolia, sepolia } from "wagmi/chains"; - -const chains = [sepolia, baseSepolia, arcTestnet] as const; - -export const config = createConfig({ - chains, - multiInjectedProviderDiscovery: false, - ssr: true, - transports: { - [sepolia.id]: http(), - [baseSepolia.id]: http("https://sepolia-preconf.base.org"), - [arcTestnet.id]: http("https://rpc.testnet.arc.network"), - }, -}); - -declare module "wagmi" { - interface Register { - config: typeof config; - } -} +// wagmi removed - using @dynamic-labs-sdk/evm/viem instead +export {}; diff --git a/examples/nextjs-sidebar/app/ClientWrapper.js b/examples/nextjs-sidebar/app/ClientWrapper.js index 99a0587..4768e60 100644 --- a/examples/nextjs-sidebar/app/ClientWrapper.js +++ b/examples/nextjs-sidebar/app/ClientWrapper.js @@ -1,24 +1,23 @@ 'use client'; import { useState, useEffect } from 'react'; -import { useDynamicContext, DynamicWidget, DynamicUserProfile, getAuthToken } from "@dynamic-labs/sdk-react-core"; +import { useUser } from "@dynamic-labs-sdk/react-hooks"; +import { getAuthToken } from "@dynamic-labs-sdk/client"; import Image from 'next/image'; +import DynamicButton from '../components/dynamic-button'; +import { dynamicClient } from '../lib/dynamic'; export default function ClientWrapper({ children }) { - const { user, setShowAuthFlow, setShowDynamicUserProfile } = useDynamicContext(); + const user = useUser(); const [isVerifying, setIsVerifying] = useState(false); const [verificationError, setVerificationError] = useState(null); - const handleLogin = () => { - setShowAuthFlow(true); - }; - useEffect(() => { const verifyToken = async () => { if (user) { setIsVerifying(true); try { - const token = await getAuthToken(); + const token = await getAuthToken(dynamicClient); const response = await fetch('/api/verify-jwt', { method: 'POST', headers: { @@ -41,29 +40,12 @@ export default function ClientWrapper({ children }) { }; verifyToken(); - }, [user, getAuthToken]); - - const SpinnerButton = ({ onClick, children }) => ( - - ); + }, [user]); return ( <>
-
- {user ? ( - setShowDynamicUserProfile(true)}> - My Dynamic Profile - - ) : ( - - Open Dynamic Sidebar Widget - - )} +
@@ -97,15 +71,7 @@ export default function ClientWrapper({ children }) { Experience the future of Web3 interactions with Dynamic's sleek Sidebar Widget. Inspired by industry leaders like Uniswap, Phantom, and Zerion, we've created a compact, comprehensive wallet control panel that seamlessly integrates with your website.

- {user ? ( - setShowDynamicUserProfile(true)}> - My Dynamic Profile - - ) : ( - - Open Dynamic Sidebar Widget - - )} + - + {children} - + ); diff --git a/examples/nextjs-sidebar/app/providers.js b/examples/nextjs-sidebar/app/providers.js new file mode 100644 index 0000000..d3c870d --- /dev/null +++ b/examples/nextjs-sidebar/app/providers.js @@ -0,0 +1,49 @@ +"use client"; + +import { useEffect } from "react"; +import { DynamicProvider, useEvent } from "@dynamic-labs-sdk/react-hooks"; +import { completeSocialRedirect, detectSocialRedirectUrl } from "@dynamic-labs-sdk/client"; +import { createWaasWalletAccounts, getChainsMissingWaasWalletAccounts } from "@dynamic-labs-sdk/client/waas"; +import { dynamicClient, initDynamic } from "../lib/dynamic"; + +function DynamicBootstrap() { + useEffect(() => { + let cancelled = false; + initDynamic().then(async () => { + if (cancelled || typeof window === "undefined") return; + try { + const url = new URL(window.location.href); + if (await detectSocialRedirectUrl({ url })) { + await completeSocialRedirect({ url }); + window.history.replaceState({}, "", window.location.pathname); + } + } catch { } + }); + return () => { cancelled = true; }; + }, []); + return null; +} + +function WalletBootstrap() { + useEvent({ + event: "userChanged", + listener: async (user) => { + if (!user) return; + const missing = getChainsMissingWaasWalletAccounts(dynamicClient); + if (missing.length > 0) { + await createWaasWalletAccounts({ chains: missing }, dynamicClient); + } + }, + }); + return null; +} + +export default function Providers({ children }) { + return ( + + + + {children} + + ); +} diff --git a/examples/nextjs-sidebar/components/dynamic-button.js b/examples/nextjs-sidebar/components/dynamic-button.js new file mode 100644 index 0000000..38db225 --- /dev/null +++ b/examples/nextjs-sidebar/components/dynamic-button.js @@ -0,0 +1,347 @@ +"use client"; + +import { useState, useRef, useEffect } from "react"; +import { useUser, useWalletAccounts, useInitStatus } from "@dynamic-labs-sdk/react-hooks"; +import { signInWithSocialRedirect, logout, sendEmailOTP, verifyOTP } from "@dynamic-labs-sdk/client"; +import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm"; +import { dynamicClient } from "../lib/dynamic"; + +function truncate(addr) { + if (!addr || addr.length < 10) return addr; + return `${addr.slice(0, 6)}…${addr.slice(-4)}`; +} + +function truncateEmail(email, max = 22) { + if (email.length <= max) return email; + const [local, domain] = email.split("@"); + if (!domain) return `${email.slice(0, max - 1)}…`; + const keep = Math.max(3, max - domain.length - 2); + return `${local.slice(0, keep)}…@${domain}`; +} + +function AddressRow({ label, addr, copied, onCopy }) { + return ( +
+
+

{label}

+

{truncate(addr)}

+
+ +
+ ); +} + +export default function DynamicButton() { + const user = useUser(); + const accounts = useWalletAccounts(); + const initStatus = useInitStatus(); + const loggedIn = user !== null; + + const [open, setOpen] = useState(false); + const [step, setStep] = useState("menu"); + const [email, setEmail] = useState(""); + const [otp, setOtp] = useState(""); + const [otpVerification, setOtpVerification] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [copied, setCopied] = useState(null); + const ref = useRef(null); + + useEffect(() => { + function onClick(e) { + if (ref.current && !ref.current.contains(e.target)) { + setOpen(false); + setStep("menu"); + setError(null); + } + } + document.addEventListener("mousedown", onClick); + return () => document.removeEventListener("mousedown", onClick); + }, []); + + const evmWallet = accounts.find(isEvmWalletAccount); + + const resetAuth = () => { + setEmail(""); + setOtp(""); + setOtpVerification(null); + }; + + const copy = (addr, key) => { + navigator.clipboard.writeText(addr); + setCopied(key); + setTimeout(() => setCopied(null), 2000); + }; + + const handleGoogle = async () => { + setLoading(true); + setError(null); + try { + await signInWithSocialRedirect( + { provider: "google", redirectUrl: globalThis.location.origin }, + dynamicClient + ); + } catch (e) { + setError(e instanceof Error ? e.message : "Google sign-in failed."); + } finally { + setLoading(false); + } + }; + + const handleEmail = async () => { + if (!email) return; + setLoading(true); + setError(null); + try { + setOtpVerification(await sendEmailOTP({ email }, dynamicClient)); + setStep("otp"); + } catch { + setError("Failed to send code. Please try again."); + } finally { + setLoading(false); + } + }; + + const handleVerify = async () => { + if (!otp || !otpVerification) return; + setLoading(true); + setError(null); + try { + await verifyOTP({ otpVerification, verificationToken: otp }, dynamicClient); + setOpen(false); + setStep("menu"); + resetAuth(); + } catch { + setError("Invalid code. Please try again."); + } finally { + setLoading(false); + } + }; + + if (initStatus !== "finished") { + return ( + + ); + } + + if (loggedIn) { + const primary = evmWallet?.address ?? ""; + const userEmail = user?.email ?? ""; + const label = userEmail ? truncateEmail(userEmail) : truncate(primary); + const addrStart = primary.startsWith("0x") ? 2 : 0; + const initials = (userEmail ? userEmail.slice(0, 2) : primary.slice(addrStart, 2)).toUpperCase(); + + return ( +
+ + {open && ( +
+
+

Signed in as

+

{user?.email ?? "—"}

+
+
+ {accounts.length === 0 ? ( +
+ + + + + Setting up your wallets... +
+ ) : ( + evmWallet && ( + copy(evmWallet.address, "evm")} + /> + ) + )} +
+ +
+ )} +
+ ); + } + + return ( +
+ + + {open && ( +
+ {error && ( +

{error}

+ )} + + {step === "menu" && ( + <> + + + + )} + + {step === "email" && ( + <> + +

Enter your email

+ setEmail(e.target.value)} + placeholder="you@example.com" + className="w-full px-3 py-2 text-sm rounded-lg border border-gray-300 bg-white text-gray-900 outline-none focus:ring-2 focus:ring-blue-300" + onKeyDown={(e) => e.key === "Enter" && handleEmail()} + /> + + + )} + + {step === "otp" && ( + <> + +

+ Code sent to {email} +

+ setOtp(e.target.value.replace(/\D/g, "").slice(0, 6))} + placeholder="Enter 6-digit code" + className="w-full px-3 py-2 text-sm rounded-lg border border-gray-300 bg-white text-gray-900 text-center tracking-widest outline-none focus:ring-2 focus:ring-blue-300" + onKeyDown={(e) => e.key === "Enter" && handleVerify()} + /> + + + )} +
+ )} +
+ ); +} diff --git a/examples/nextjs-sidebar/lib/dynamic.js b/examples/nextjs-sidebar/lib/dynamic.js new file mode 100644 index 0000000..65f2f47 --- /dev/null +++ b/examples/nextjs-sidebar/lib/dynamic.js @@ -0,0 +1,17 @@ +import { createDynamicClient, initializeClient } from "@dynamic-labs-sdk/client"; +import { addWaasEvmExtension } from "@dynamic-labs-sdk/evm/waas"; + +export const dynamicClient = createDynamicClient({ + environmentId: process.env.NEXT_PUBLIC_DYNAMIC_ENV_ID, + autoInitialize: false, + metadata: { name: "Dynamic Sidebar Widget Demo" }, +}); + +let initialized = false; + +export async function initDynamic() { + if (initialized) return; + initialized = true; + addWaasEvmExtension(dynamicClient); + await initializeClient(dynamicClient); +} diff --git a/examples/nextjs-sidebar/package.json b/examples/nextjs-sidebar/package.json index 20a71fa..6d6595a 100644 --- a/examples/nextjs-sidebar/package.json +++ b/examples/nextjs-sidebar/package.json @@ -9,8 +9,9 @@ "lint": "next lint" }, "dependencies": { - "@dynamic-labs/ethereum": "4.48.2", - "@dynamic-labs/sdk-react-core": "4.48.2", + "@dynamic-labs-sdk/client": "1.4.0", + "@dynamic-labs-sdk/evm": "1.4.0", + "@dynamic-labs-sdk/react-hooks": "1.4.0", "jsonwebtoken": "9.0.2", "jwks-rsa": "3.1.0", "next": "14.2.35", diff --git a/examples/nextjs-stablecoin-card-rain/components/account/account-modal.tsx b/examples/nextjs-stablecoin-card-rain/components/account/account-modal.tsx index c5a4cb3..f39926d 100644 --- a/examples/nextjs-stablecoin-card-rain/components/account/account-modal.tsx +++ b/examples/nextjs-stablecoin-card-rain/components/account/account-modal.tsx @@ -2,9 +2,11 @@ import { useState, useMemo, useRef, useEffect } from "react"; import { Copy, Check, LogOut, Coins, Wallet2, CreditCard } from "lucide-react"; -import { useDynamicContext } from "@/lib/dynamic"; +import { useUser, useWalletAccounts } from "@dynamic-labs-sdk/react-hooks"; +import { logout } from "@dynamic-labs-sdk/client"; +import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm"; +import { dynamicClient } from "@/lib/dynamic"; import { useQuery } from "@tanstack/react-query"; -import { getAuthToken } from "@dynamic-labs/sdk-react-core"; import { Modal } from "@/components/ui/modal"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; @@ -21,19 +23,21 @@ interface AccountModalProps { } export default function AccountModal({ isOpen, onClose }: AccountModalProps) { - const { primaryWallet, network, handleLogOut, user } = useDynamicContext(); - const enabledNetworks = primaryWallet?.connector.getEnabledNetworks(); - const authToken = getAuthToken(); + const user = useUser(); + const { walletAccounts } = useWalletAccounts(); + const evmWallet = walletAccounts?.find(isEvmWalletAccount); + const authToken = dynamicClient.auth.token; const { getBalanceByAddress, isLoading: isLoadingBalances } = useTokenBalanceContext(); const [copied, setCopied] = useState(null); const [addressContainerWidth, setAddressContainerWidth] = useState(0); const addressContainerRef = useRef(null); + const chainId = evmWallet?.network?.id; const rusdcAddress = useMemo(() => { - if (!network) return undefined; - return getContractAddress(network, "RUSDC"); - }, [network]); + if (!chainId) return undefined; + return getContractAddress(chainId, "RUSDC"); + }, [chainId]); const walletBalance = getBalanceByAddress(rusdcAddress || ""); // Fetch card balance @@ -72,30 +76,27 @@ export default function AccountModal({ isOpen, onClose }: AccountModalProps) { }; const handleLogout = () => { - handleLogOut(); + logout(dynamicClient); onClose(); }; - // Resolve current network info directly from Dynamic's enabled networks + // Resolve current network info from evmWallet const currentNetwork = useMemo(() => { - const found = enabledNetworks?.find( - (n) => String(n.chainId || n.networkId) === String(network) - ); - - if (!found) { + const network = evmWallet?.network; + if (!network) { return { - id: String(network || ""), - name: `Network ${network ?? ""}`, + id: "", + name: "Unknown Network", iconUrl: undefined, }; } return { - id: String(found.chainId || found.networkId || ""), - name: found.vanityName || found.name, - iconUrl: found.iconUrls?.[0] || undefined, + id: String(network.id), + name: network.name ?? `Network ${network.id}`, + iconUrl: undefined, }; - }, [primaryWallet, network]); + }, [evmWallet]); return ( @@ -125,15 +126,7 @@ export default function AccountModal({ isOpen, onClose }: AccountModalProps) {

{user?.email}

- {currentNetwork.iconUrl ? ( - - ) : ( - - )} +

{currentNetwork.name}

@@ -143,8 +136,8 @@ export default function AccountModal({ isOpen, onClose }: AccountModalProps) { size="sm" className="h-8 w-8 p-0 text-white hover:bg-white/20" onClick={() => - primaryWallet?.address && - copyToClipboard(primaryWallet.address, "address") + evmWallet?.address && + copyToClipboard(evmWallet.address, "address") } > {copied === "address" ? ( @@ -158,8 +151,8 @@ export default function AccountModal({ isOpen, onClose }: AccountModalProps) {

Address

- {primaryWallet?.address - ? formatAddress(primaryWallet.address, addressContainerWidth) + {evmWallet?.address + ? formatAddress(evmWallet.address, addressContainerWidth) : "Not connected"}
diff --git a/examples/nextjs-stablecoin-card-rain/components/account/network-selector.tsx b/examples/nextjs-stablecoin-card-rain/components/account/network-selector.tsx index 6b73fea..a3755d4 100644 --- a/examples/nextjs-stablecoin-card-rain/components/account/network-selector.tsx +++ b/examples/nextjs-stablecoin-card-rain/components/account/network-selector.tsx @@ -1,137 +1,10 @@ "use client"; -import { useMemo, useState } from "react"; -import { ChevronDown } from "lucide-react"; -import { useDynamicContext } from "@/lib/dynamic"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; +// Network switching is not supported for embedded WaaS wallets. +// This component returns null to avoid rendering a broken UI. -interface AccountModalProps { - currentNetwork: { - id: string; - name: string; - iconUrl: string | undefined; - }; -} - -export default function NetworkSelector({ currentNetwork }: AccountModalProps) { - const { primaryWallet } = useDynamicContext(); - const enabledNetworks = primaryWallet?.connector.getEnabledNetworks(); - const [switchingToNetworkId, setSwitchingToNetworkId] = useState< - string | null - >(null); - const [isLoading, setIsLoading] = useState(false); - - const handleNetworkSwitch = async (networkId: string | number) => { - if (!primaryWallet?.connector.supportsNetworkSwitching()) return; - if (isLoading) return; // Prevent multiple simultaneous attempts - - const networkIdString = String(networkId); - - // Don't switch if already on the target network - if (currentNetwork.id === networkIdString) return; - - setSwitchingToNetworkId(networkIdString); - setIsLoading(true); - - try { - await primaryWallet.switchNetwork(networkId); - // Switch completed successfully - } catch (error) { - console.error("Network switch failed:", error); - // Optionally show user-friendly error message - } finally { - setSwitchingToNetworkId(null); - setIsLoading(false); - } - }; - - // Get available networks from Dynamic SDK - const availableNetworks = useMemo(() => { - if (!primaryWallet || !enabledNetworks || enabledNetworks.length === 0) { - return []; - } - - return enabledNetworks.map((networkConfig) => ({ - id: String(networkConfig.chainId || networkConfig.networkId), - name: networkConfig.vanityName || networkConfig.name, - iconUrl: networkConfig.iconUrls?.[0] as string | undefined, - })); - }, [primaryWallet, enabledNetworks]); - - if (!primaryWallet?.connector.supportsNetworkSwitching()) return null; - return ( - <> -
-
-
- Network - - - - - - {availableNetworks.map((net) => { - const isSwitchingToThis = - switchingToNetworkId === net.id && isLoading; - const isCurrentNetwork = currentNetwork.id === net.id; - - return ( - handleNetworkSwitch(net.id)} - className={`flex items-center gap-2 cursor-pointer ${ - isLoading && !isSwitchingToThis - ? "opacity-50 cursor-not-allowed" - : "" - } ${isCurrentNetwork ? "bg-accent" : ""}`} - disabled={isLoading && !isSwitchingToThis} - > - {net.iconUrl ? ( - - ) : ( - - )} - {net.name} - {isSwitchingToThis && ( - - Switching... - - )} - {isCurrentNetwork && !isSwitchingToThis && ( - - Current - - )} - - ); - })} - - -
-
- - ); +export default function NetworkSelector(_props: { + currentNetwork: { id: string; name: string; iconUrl: string | undefined }; +}) { + return null; } diff --git a/examples/nextjs-stablecoin-card-rain/components/application/application-form.tsx b/examples/nextjs-stablecoin-card-rain/components/application/application-form.tsx index f42f320..87ac971 100644 --- a/examples/nextjs-stablecoin-card-rain/components/application/application-form.tsx +++ b/examples/nextjs-stablecoin-card-rain/components/application/application-form.tsx @@ -16,12 +16,9 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { OCCUPATION_OPTIONS, US_STATES } from "@/constants"; -import { - getAuthToken, - useDynamicContext, - useIsLoggedIn, - useRefreshUser, -} from "@/lib/dynamic"; +import { getAuthToken } from "@dynamic-labs-sdk/client"; +import { useUser, useInitStatus } from "@dynamic-labs-sdk/react-hooks"; +import { dynamicClient } from "@/lib/dynamic"; import type { CreateCardForUserResponse } from "@/lib/rain"; import { cn } from "@/lib/utils"; import { @@ -33,9 +30,10 @@ import { formatSSN } from "@/utils/format-ssn"; import { defaultValues, FormSchema, STEPS } from "./helpers"; export default function ApplicationForm({ formId }: { formId: string }) { - const { sdkHasLoaded, user, setShowAuthFlow } = useDynamicContext(); - const refreshUser = useRefreshUser(); - const isLoggedIn = useIsLoggedIn(); + const user = useUser(); + const initStatus = useInitStatus(); + const isLoggedIn = user !== null; + const sdkHasLoaded = initStatus === "finished"; useEffect(() => { const metadata = user?.metadata as { rainCard?: CreateCardForUserResponse }; @@ -44,9 +42,10 @@ export default function ApplicationForm({ formId }: { formId: string }) { useEffect(() => { if (sdkHasLoaded && !isLoggedIn) { - setShowAuthFlow(true); + // Show auth flow via dynamicClient UI + dynamicClient.ui.auth.show(); } - }, [sdkHasLoaded, isLoggedIn, setShowAuthFlow]); + }, [sdkHasLoaded, isLoggedIn]); const [currentStep, setCurrentStep] = useState(0); const [isSubmitting, setIsSubmitting] = useState(false); @@ -97,7 +96,7 @@ export default function ApplicationForm({ formId }: { formId: string }) { setSubmitResult(null); try { - const authToken = getAuthToken(); + const authToken = getAuthToken(dynamicClient); if (!authToken) throw new Error("Not authenticated"); const response = await fetch("/api/apply", { @@ -116,7 +115,6 @@ export default function ApplicationForm({ formId }: { formId: string }) { } setSubmitResult({ ok: true, data: result }); - refreshUser(); redirect("/card"); } catch (error) { const errorMessage = diff --git a/examples/nextjs-stablecoin-card-rain/components/dynamic-card/card-balance.tsx b/examples/nextjs-stablecoin-card-rain/components/dynamic-card/card-balance.tsx index 27d2ba2..c910e53 100644 --- a/examples/nextjs-stablecoin-card-rain/components/dynamic-card/card-balance.tsx +++ b/examples/nextjs-stablecoin-card-rain/components/dynamic-card/card-balance.tsx @@ -2,7 +2,7 @@ import { RefreshCw } from "lucide-react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { getAuthToken } from "@dynamic-labs/sdk-react-core"; +import { dynamicClient } from "@/lib/dynamic"; import { Skeleton } from "../ui/skeleton"; import { UserCreditBalanceResponse } from "@/lib/rain"; @@ -10,7 +10,7 @@ import { cn } from "@/lib/utils"; import { formatBalance } from "@/utils/format-balance"; export default function CardBalance() { - const authToken = getAuthToken(); + const authToken = dynamicClient.auth.token; const queryClient = useQueryClient(); const { data, isLoading, isRefetching, refetch } = useQuery<{ diff --git a/examples/nextjs-stablecoin-card-rain/components/dynamic-card/fund-card.tsx b/examples/nextjs-stablecoin-card-rain/components/dynamic-card/fund-card.tsx index b7d0f68..39747ff 100644 --- a/examples/nextjs-stablecoin-card-rain/components/dynamic-card/fund-card.tsx +++ b/examples/nextjs-stablecoin-card-rain/components/dynamic-card/fund-card.tsx @@ -7,7 +7,10 @@ import { Loader2, Plus } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Modal } from "@/components/ui/modal"; -import { getAuthToken, useDynamicContext } from "@/lib/dynamic"; +import { getAuthToken } from "@dynamic-labs-sdk/client"; +import { useUser, useWalletAccounts, useInitStatus } from "@dynamic-labs-sdk/react-hooks"; +import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm"; +import { dynamicClient } from "@/lib/dynamic"; import { UserDepositContractResponse } from "@/lib/rain"; import { useDepositToken } from "@/hooks/use-deposit-tokens"; import { getContractAddress } from "@/constants"; @@ -15,12 +18,17 @@ import DepositAccountLoading from "./deposit-account-loading"; import WalletBalanceDisplay from "./wallet-balance-display"; import { useTokenBalanceContext } from "./token-balance-context"; +const BASE_SEPOLIA_CHAIN_ID = 84532; const PRESET_AMOUNTS = [5, 10, 25]; export default function FundCard() { const queryClient = useQueryClient(); const { depositToken } = useDepositToken(); - const { sdkHasLoaded, primaryWallet, network } = useDynamicContext(); + const accounts = useWalletAccounts(); + const initStatus = useInitStatus(); + const primaryWallet = accounts.find(isEvmWalletAccount) ?? null; + const network: number | null = primaryWallet ? BASE_SEPOLIA_CHAIN_ID : null; + const sdkHasLoaded = initStatus === "finished"; const { getBalanceByAddress, refetch: fetchAccountBalances } = useTokenBalanceContext(); @@ -50,7 +58,7 @@ export default function FundCard() { }, retryDelay: 1500, queryFn: async () => { - const authToken = getAuthToken(); + const authToken = getAuthToken(dynamicClient); const response = await fetch(`/api/contracts?chain=${network}`, { headers: { Authorization: `Bearer ${authToken}` }, }); diff --git a/examples/nextjs-stablecoin-card-rain/components/dynamic-card/index.tsx b/examples/nextjs-stablecoin-card-rain/components/dynamic-card/index.tsx index 5423000..47a73fa 100644 --- a/examples/nextjs-stablecoin-card-rain/components/dynamic-card/index.tsx +++ b/examples/nextjs-stablecoin-card-rain/components/dynamic-card/index.tsx @@ -9,7 +9,7 @@ import { CreditCard } from "@/components/credit-cards"; import DynamicLogo from "@/components/dynamic/logo"; import { Button } from "@/components/ui/button"; -import { useIsLoggedIn, useDynamicContext } from "@/lib/dynamic"; +import { useUser, useInitStatus } from "@dynamic-labs-sdk/react-hooks"; import { CreateCardForUserResponse } from "@/lib/rain/types"; import { Skeleton } from "../ui/skeleton"; @@ -22,8 +22,10 @@ import StablecoinFaucet from "./stablecoin-faucet"; import { TokenBalanceProvider } from "./token-balance-context"; export default function DynamicCard() { - const isLoggedIn = useIsLoggedIn(); - const { sdkHasLoaded, user } = useDynamicContext(); + const user = useUser(); + const initStatus = useInitStatus(); + const isLoggedIn = user !== null; + const sdkHasLoaded = initStatus === "finished"; const [hasMounted, setHasMounted] = useState(false); const [decryptedCardData, setDecryptedCardData] = useState<{ diff --git a/examples/nextjs-stablecoin-card-rain/components/dynamic-card/stablecoin-faucet.tsx b/examples/nextjs-stablecoin-card-rain/components/dynamic-card/stablecoin-faucet.tsx index fe4bdfd..75e43d1 100644 --- a/examples/nextjs-stablecoin-card-rain/components/dynamic-card/stablecoin-faucet.tsx +++ b/examples/nextjs-stablecoin-card-rain/components/dynamic-card/stablecoin-faucet.tsx @@ -5,7 +5,6 @@ import { ArrowRight, Droplets, Loader2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useMintTokens } from "@/hooks/use-mint-tokens"; -import { useDynamicContext } from "@/lib/dynamic"; import { getContractAddress } from "@/constants"; import WalletBalanceDisplay from "./wallet-balance-display"; import { @@ -16,16 +15,16 @@ import { } from "@/components/ui/tooltip"; import { useTokenBalanceContext } from "./token-balance-context"; +const BASE_SEPOLIA_CHAIN_ID = 84532; + export default function StablecoinFaucet() { const { refetch: refetchBalances } = useTokenBalanceContext(); const [fundingWallet, setFundingWallet] = useState(false); - const { network } = useDynamicContext(); const rusdcAddress = useMemo(() => { - if (!network) return undefined; - return getContractAddress(network, "RUSDC"); - }, [network]); + return getContractAddress(BASE_SEPOLIA_CHAIN_ID, "RUSDC"); + }, []); const { mintTokens, resetMint, isPending } = useMintTokens({ onMintSuccess: () => { diff --git a/examples/nextjs-stablecoin-card-rain/components/dynamic-card/token-balance-context.tsx b/examples/nextjs-stablecoin-card-rain/components/dynamic-card/token-balance-context.tsx index 8fb6f61..f650493 100644 --- a/examples/nextjs-stablecoin-card-rain/components/dynamic-card/token-balance-context.tsx +++ b/examples/nextjs-stablecoin-card-rain/components/dynamic-card/token-balance-context.tsx @@ -1,11 +1,33 @@ "use client"; -import { createContext, useContext, useEffect, useMemo } from "react"; -import { - useDynamicContext, - useTokenBalances, - TokenBalance, -} from "@/lib/dynamic"; +import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"; +import { createPublicClient, http, type Address } from "viem"; +import { baseSepolia } from "viem/chains"; +import { useUser, useWalletAccounts } from "@dynamic-labs-sdk/react-hooks"; +import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm"; + +const ERC20_BALANCE_ABI = [ + { + inputs: [{ name: "account", type: "address" }], + name: "balanceOf", + outputs: [{ name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "decimals", + outputs: [{ name: "", type: "uint8" }], + stateMutability: "view", + type: "function", + }, +] as const; + +export interface TokenBalance { + address: string; + balance: number; + symbol?: string; +} interface TokenBalanceProviderProps { children: React.ReactNode; @@ -22,30 +44,87 @@ const TokenBalanceContext = createContext( undefined ); +const publicClient = createPublicClient({ + chain: baseSepolia, + transport: http(), +}); + export function TokenBalanceProvider({ children }: TokenBalanceProviderProps) { - const { sdkHasLoaded, primaryWallet, network } = useDynamicContext(); + const user = useUser(); + const accounts = useWalletAccounts(); + const primaryWallet = accounts.find(isEvmWalletAccount) ?? null; + + const [balances, setBalances] = useState(undefined); + const [isLoading, setIsLoading] = useState(false); + const [trackedAddresses, setTrackedAddresses] = useState([]); + + const fetchBalances = useCallback( + async (force = false) => { + if (!primaryWallet?.address || trackedAddresses.length === 0) return; + setIsLoading(true); + try { + const results = await Promise.all( + trackedAddresses.map(async (tokenAddress) => { + try { + const [rawBalance, decimals] = await Promise.all([ + publicClient.readContract({ + address: tokenAddress as Address, + abi: ERC20_BALANCE_ABI, + functionName: "balanceOf", + args: [primaryWallet.address as Address], + }), + publicClient.readContract({ + address: tokenAddress as Address, + abi: ERC20_BALANCE_ABI, + functionName: "decimals", + }), + ]); + const balance = Number(rawBalance) / 10 ** Number(decimals); + return { address: tokenAddress, balance } as TokenBalance; + } catch { + return { address: tokenAddress, balance: 0 } as TokenBalance; + } + }) + ); + setBalances(results); + } catch { + // no-op + } finally { + setIsLoading(false); + } + }, + [primaryWallet?.address, trackedAddresses] + ); - const { tokenBalances, isLoading, fetchAccountBalances } = useTokenBalances({ - networkId: Number(network), - accountAddress: primaryWallet?.address, - }); + useEffect(() => { + if (primaryWallet?.address && trackedAddresses.length > 0) { + fetchBalances(); + } + }, [primaryWallet?.address, trackedAddresses, fetchBalances]); - const getBalanceByAddress = useMemo( - () => - (address: string): TokenBalance | undefined => { - return tokenBalances?.find( - (t) => t.address.toLowerCase() === address.toLowerCase() + const getBalanceByAddress = useCallback( + (address: string): TokenBalance | undefined => { + if (!balances) return undefined; + const found = balances.find( + (t) => t.address.toLowerCase() === address.toLowerCase() + ); + if (!found && address) { + // Register this address for tracking + setTrackedAddresses((prev) => + prev.includes(address.toLowerCase()) ? prev : [...prev, address.toLowerCase()] ); - }, - [tokenBalances] + } + return found; + }, + [balances] ); return ( diff --git a/examples/nextjs-stablecoin-card-rain/components/dynamic-card/withdraw-funds.tsx b/examples/nextjs-stablecoin-card-rain/components/dynamic-card/withdraw-funds.tsx index a54f4d2..8d5d1ec 100644 --- a/examples/nextjs-stablecoin-card-rain/components/dynamic-card/withdraw-funds.tsx +++ b/examples/nextjs-stablecoin-card-rain/components/dynamic-card/withdraw-funds.tsx @@ -12,17 +12,26 @@ import { TooltipTrigger, TooltipContent, } from "@/components/ui/tooltip"; -import { getAuthToken, useDynamicContext } from "@/lib/dynamic"; +import { getAuthToken } from "@dynamic-labs-sdk/client"; +import { useUser, useWalletAccounts, useInitStatus } from "@dynamic-labs-sdk/react-hooks"; +import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm"; +import { dynamicClient } from "@/lib/dynamic"; import { UserDepositContractResponse, UserWithdrawalRequest } from "@/lib/rain"; import { useWithdrawAsset } from "@/hooks/use-withdraw-asset"; import { getContractAddress } from "@/constants"; import { formatBalance } from "@/utils/format-balance"; +const BASE_SEPOLIA_CHAIN_ID = 84532; const PRESET_AMOUNTS = [5, 10, 25, 50]; export default function WithdrawFunds() { - const authToken = getAuthToken(); - const { sdkHasLoaded, primaryWallet, network, user } = useDynamicContext(); + const user = useUser(); + const accounts = useWalletAccounts(); + const initStatus = useInitStatus(); + const primaryWallet = accounts.find(isEvmWalletAccount) ?? null; + const network: number | null = primaryWallet ? BASE_SEPOLIA_CHAIN_ID : null; + const sdkHasLoaded = initStatus === "finished"; + const authToken = getAuthToken(dynamicClient); const { withdrawAsset, isPending: isWithdrawPending } = useWithdrawAsset(); const [amount, setAmount] = useState(""); const [error, setError] = useState(""); @@ -45,7 +54,7 @@ export default function WithdrawFunds() { queryKey: ["contracts", primaryWallet?.address, network], enabled: !!sdkHasLoaded && !!primaryWallet && !!network, queryFn: async () => { - const authToken = getAuthToken(); + const authToken = getAuthToken(dynamicClient); const response = await fetch(`/api/contracts?chain=${network}`, { headers: { Authorization: `Bearer ${authToken}` }, }); diff --git a/examples/nextjs-stablecoin-card-rain/components/dynamic-methods.tsx b/examples/nextjs-stablecoin-card-rain/components/dynamic-methods.tsx index d7f25d3..1d06c8f 100644 --- a/examples/nextjs-stablecoin-card-rain/components/dynamic-methods.tsx +++ b/examples/nextjs-stablecoin-card-rain/components/dynamic-methods.tsx @@ -3,20 +3,18 @@ import { useState, useEffect } from "react"; import { redirect } from "next/navigation"; import { Copy, Check } from "lucide-react"; -import { - useDynamicContext, - useIsLoggedIn, - useUserWallets, -} from "@/lib/dynamic"; +import { useUser, useWalletAccounts } from "@dynamic-labs-sdk/react-hooks"; +import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm"; +import { createWalletClientForWalletAccount } from "@dynamic-labs-sdk/evm/viem"; +import { createPublicClient, http } from "viem"; import DynamicWidget from "./dynamic/dynamic-widget"; import { Button } from "./ui/button"; import { Skeleton } from "./ui/skeleton"; -import { isEthereumWallet } from "@dynamic-labs/ethereum"; export default function DynamicMethods() { - const isLoggedIn = useIsLoggedIn(); - const { sdkHasLoaded, primaryWallet, user } = useDynamicContext(); - const userWallets = useUserWallets(); + const user = useUser(); + const walletAccounts = useWalletAccounts(); + const evmWallet = walletAccounts.find(isEvmWalletAccount); const [isLoading, setIsLoading] = useState(true); const [result, setResult] = useState(""); @@ -25,8 +23,8 @@ export default function DynamicMethods() { const [copied, setCopied] = useState(false); useEffect(() => { - if (sdkHasLoaded && !isLoggedIn) redirect("/"); - }, [sdkHasLoaded, isLoggedIn]); + if (!user) redirect("/"); + }, [user]); const safeStringify = (obj: unknown): string => { const seen = new WeakSet(); @@ -46,12 +44,12 @@ export default function DynamicMethods() { }; useEffect(() => { - if (sdkHasLoaded && isLoggedIn && primaryWallet) { + if (user && evmWallet) { setIsLoading(false); } else { setIsLoading(true); } - }, [sdkHasLoaded, isLoggedIn, primaryWallet]); + }, [user, evmWallet]); function clearResult() { setResult(""); @@ -71,7 +69,7 @@ export default function DynamicMethods() { function showUserWallets() { try { - setResult(safeStringify(userWallets)); + setResult(safeStringify(walletAccounts)); setError(null); } catch (err) { setError( @@ -81,11 +79,11 @@ export default function DynamicMethods() { } async function fetchEthereumPublicClient() { - if (!primaryWallet || !isEthereumWallet(primaryWallet)) return; + if (!evmWallet) return; try { setIsLoading(true); - const result = await primaryWallet.getPublicClient(); - setResult(safeStringify(result)); + const publicClient = createPublicClient({ chain: evmWallet.network, transport: http() }); + setResult(safeStringify(publicClient)); } catch (error) { setResult( safeStringify({ @@ -99,11 +97,11 @@ export default function DynamicMethods() { } async function fetchEthereumWalletClient() { - if (!primaryWallet || !isEthereumWallet(primaryWallet)) return; + if (!evmWallet) return; try { setIsLoading(true); - const result = await primaryWallet.getWalletClient(); - setResult(safeStringify(result)); + const walletClient = await createWalletClientForWalletAccount({ walletAccount: evmWallet }); + setResult(safeStringify(walletClient)); } catch (error) { setResult( safeStringify({ @@ -117,10 +115,14 @@ export default function DynamicMethods() { } async function signEthereumMessage() { - if (!primaryWallet || !isEthereumWallet(primaryWallet)) return; + if (!evmWallet) return; try { setIsLoading(true); - const result = await primaryWallet.signMessage("Hello World"); + const walletClient = await createWalletClientForWalletAccount({ walletAccount: evmWallet }); + const result = await walletClient.signMessage({ + account: evmWallet.address as `0x${string}`, + message: "Hello World", + }); setResult(safeStringify(result)); } catch (error) { setResult( @@ -206,7 +208,7 @@ export default function DynamicMethods() { > Fetch User Wallets - {primaryWallet && isEthereumWallet(primaryWallet) && ( + {evmWallet && (
Wallet Methods diff --git a/examples/nextjs-stablecoin-card-rain/components/dynamic/dynamic-embedded-widget.tsx b/examples/nextjs-stablecoin-card-rain/components/dynamic/dynamic-embedded-widget.tsx index 487b291..d8062f4 100644 --- a/examples/nextjs-stablecoin-card-rain/components/dynamic/dynamic-embedded-widget.tsx +++ b/examples/nextjs-stablecoin-card-rain/components/dynamic/dynamic-embedded-widget.tsx @@ -1,7 +1,7 @@ "use client"; -import { DynamicEmbeddedWidget as DynamicEmbeddedWidgetComponent } from "@/lib/dynamic"; +import DynamicButton from "./dynamic-widget"; export default function DynamicEmbeddedWidget() { - return ; + return ; } diff --git a/examples/nextjs-stablecoin-card-rain/components/dynamic/dynamic-logout.tsx b/examples/nextjs-stablecoin-card-rain/components/dynamic/dynamic-logout.tsx index e031d39..ea7b77f 100644 --- a/examples/nextjs-stablecoin-card-rain/components/dynamic/dynamic-logout.tsx +++ b/examples/nextjs-stablecoin-card-rain/components/dynamic/dynamic-logout.tsx @@ -1,21 +1,22 @@ "use client"; import { useState, useEffect } from "react"; -import { useDynamicContext, useIsLoggedIn } from "@/lib/dynamic"; +import { useUser } from "@dynamic-labs-sdk/react-hooks"; +import { logout } from "@dynamic-labs-sdk/client"; +import { dynamicClient } from "@/lib/dynamic"; import { Button } from "../ui/button"; export default function DynamicLogout() { - const { handleLogOut } = useDynamicContext(); - const isLoggedIn = useIsLoggedIn(); + const user = useUser(); const [hasMounted, setHasMounted] = useState(false); useEffect(() => { setHasMounted(true); }, []); - if (!isLoggedIn || !hasMounted) return null; + if (!user || !hasMounted) return null; return ( - ); diff --git a/examples/nextjs-stablecoin-card-rain/components/dynamic/dynamic-widget.tsx b/examples/nextjs-stablecoin-card-rain/components/dynamic/dynamic-widget.tsx index 1e63150..b42c220 100644 --- a/examples/nextjs-stablecoin-card-rain/components/dynamic/dynamic-widget.tsx +++ b/examples/nextjs-stablecoin-card-rain/components/dynamic/dynamic-widget.tsx @@ -1,7 +1,31 @@ "use client"; -import { DynamicWidget as DynamicWidgetComponent } from "@/lib/dynamic"; +import { useUser } from "@dynamic-labs-sdk/react-hooks"; +import { logout } from "@dynamic-labs-sdk/client"; +import { dynamicClient } from "@/lib/dynamic"; -export default function DynamicWidget() { - return ; +export function DynamicButton() { + const user = useUser(); + + if (user) { + return ( + + ); + } + + return ( + + ); } + +export default DynamicButton; diff --git a/examples/nextjs-stablecoin-card-rain/hooks/get-admin-signature.ts b/examples/nextjs-stablecoin-card-rain/hooks/get-admin-signature.ts index 38a68f3..897029b 100644 --- a/examples/nextjs-stablecoin-card-rain/hooks/get-admin-signature.ts +++ b/examples/nextjs-stablecoin-card-rain/hooks/get-admin-signature.ts @@ -1,8 +1,9 @@ -import { isEthereumWallet } from "@dynamic-labs/ethereum"; +import { type WalletClient } from "viem"; import { randomBytes } from "crypto"; type AdminSignatureOpts = { - primaryWallet: any; + walletClient: WalletClient; + signerAddress: string; chainId: number; collateralProxyAddress: string; recipientAddress: string; @@ -18,7 +19,8 @@ type AdminSignatureOpts = { */ export const getAdminSignature = async (opts: AdminSignatureOpts) => { const { - primaryWallet, + walletClient, + signerAddress, collateralProxyAddress, chainId, tokenAddress, @@ -27,12 +29,6 @@ export const getAdminSignature = async (opts: AdminSignatureOpts) => { nonce, } = opts; - if (!primaryWallet || !isEthereumWallet(primaryWallet)) { - throw new Error("Wallet not connected or not EVM compatible"); - } - - const walletClient = await primaryWallet.getWalletClient(); - const salt = `0x${randomBytes(32).toString("hex")}` as `0x${string}`; const domain = { name: "Collateral", @@ -50,17 +46,17 @@ export const getAdminSignature = async (opts: AdminSignatureOpts) => { { name: "nonce", type: "uint256" }, ], }; - const signerAddress = primaryWallet.address; const message = { - user: signerAddress, - asset: tokenAddress, + user: signerAddress as `0x${string}`, + asset: tokenAddress as `0x${string}`, amount, - recipient: recipientAddress, + recipient: recipientAddress as `0x${string}`, nonce, }; const signature = await walletClient.signTypedData({ + account: signerAddress as `0x${string}`, primaryType: "Withdraw", domain, types: type, diff --git a/examples/nextjs-stablecoin-card-rain/hooks/use-deposit-tokens.ts b/examples/nextjs-stablecoin-card-rain/hooks/use-deposit-tokens.ts index 6618fb4..1ed9dab 100644 --- a/examples/nextjs-stablecoin-card-rain/hooks/use-deposit-tokens.ts +++ b/examples/nextjs-stablecoin-card-rain/hooks/use-deposit-tokens.ts @@ -1,11 +1,9 @@ import { useState } from "react"; -import { parseUnits, erc20Abi } from "viem"; +import { parseUnits, erc20Abi, createPublicClient, http } from "viem"; -import { - useDynamicContext, - isEthereumWallet, - isZeroDevConnector, -} from "@/lib/dynamic"; +import { useWalletAccounts } from "@dynamic-labs-sdk/react-hooks"; +import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm"; +import { createWalletClientForWalletAccount } from "@dynamic-labs-sdk/evm/viem"; import { useToast } from "@/lib/toast-context"; export interface DepositTokenOptions { @@ -20,7 +18,7 @@ export interface UseDepositTokenOptions { } export function useDepositToken(options?: UseDepositTokenOptions) { - const { primaryWallet } = useDynamicContext(); + const { walletAccounts } = useWalletAccounts(); const { success } = useToast(); const [isLoading, setIsLoading] = useState(false); @@ -29,14 +27,15 @@ export function useDepositToken(options?: UseDepositTokenOptions) { const depositToken = async (depositOptions: DepositTokenOptions) => { const { amount, token, to, decimals = 18 } = depositOptions; - if (!primaryWallet || !isEthereumWallet(primaryWallet)) { + const evmWallet = walletAccounts?.find(isEvmWalletAccount); + if (!evmWallet) { throw new Error("Wallet not connected or not EVM compatible"); } setIsLoading(true); try { - const walletClient = await primaryWallet.getWalletClient(); + const walletClient = await createWalletClientForWalletAccount({ walletAccount: evmWallet }); // Convert amount to token units based on decimals const amountInUnits = parseUnits(amount, decimals); @@ -52,13 +51,8 @@ export function useDepositToken(options?: UseDepositTokenOptions) { setTxHash(hash); // Wait for transaction receipt - const connector = primaryWallet.connector; - if (!connector || !isZeroDevConnector(connector)) { - throw new Error("Connector is not a ZeroDev connector"); - } - const kernelClient = connector.getAccountAbstractionProvider(); - if (!kernelClient) throw new Error("Kernel client not found"); - await kernelClient.waitForUserOperationReceipt({ hash }); + const publicClient = createPublicClient({ chain: evmWallet.network, transport: http() }); + await publicClient.waitForTransactionReceipt({ hash }); success( "Deposit Processing", diff --git a/examples/nextjs-stablecoin-card-rain/hooks/use-mint-tokens.ts b/examples/nextjs-stablecoin-card-rain/hooks/use-mint-tokens.ts index 99d8e24..d000fd9 100644 --- a/examples/nextjs-stablecoin-card-rain/hooks/use-mint-tokens.ts +++ b/examples/nextjs-stablecoin-card-rain/hooks/use-mint-tokens.ts @@ -1,10 +1,9 @@ import { useState } from "react"; +import { createPublicClient, http } from "viem"; -import { - useDynamicContext, - isEthereumWallet, - isZeroDevConnector, -} from "@/lib/dynamic"; +import { useWalletAccounts } from "@dynamic-labs-sdk/react-hooks"; +import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm"; +import { createWalletClientForWalletAccount } from "@dynamic-labs-sdk/evm/viem"; import { useToast } from "@/lib/toast-context"; import { getContractAddress, RUSDC_ABI } from "../constants"; @@ -18,25 +17,27 @@ export interface UseMintTokensOptions { } export function useMintTokens(options?: UseMintTokensOptions) { - const { primaryWallet, network } = useDynamicContext(); + const { walletAccounts } = useWalletAccounts(); const { success, error } = useToast(); const [isLoading, setIsLoading] = useState(false); const [txHash, setTxHash] = useState(null); const mintTokens = async (mintOptions: MintOptions) => { - if (!primaryWallet || !isEthereumWallet(primaryWallet)) { + const evmWallet = walletAccounts?.find(isEvmWalletAccount); + if (!evmWallet) { throw new Error("Wallet not connected or not EVM compatible"); } - if (!network) throw new Error("Network not found"); - const rusdcAddress = getContractAddress(network, "RUSDC"); + const chainId = evmWallet.network?.id; + if (!chainId) throw new Error("Network not found"); + const rusdcAddress = getContractAddress(chainId, "RUSDC"); const { amountDollars } = mintOptions; try { setIsLoading(true); - const walletClient = await primaryWallet.getWalletClient(); + const walletClient = await createWalletClientForWalletAccount({ walletAccount: evmWallet }); // Use writeContract for ERC-20 transfers const hash = await walletClient.writeContract({ @@ -48,13 +49,9 @@ export function useMintTokens(options?: UseMintTokensOptions) { setTxHash(hash); - const connector = primaryWallet.connector; - if (!connector || !isZeroDevConnector(connector)) { - throw new Error("Connector is not a ZeroDev connector"); - } - const kernelClient = connector.getAccountAbstractionProvider(); - if (!kernelClient) throw new Error("Kernel client not found"); - await kernelClient.waitForUserOperationReceipt({ hash }); + // Wait for transaction receipt + const publicClient = createPublicClient({ chain: evmWallet.network, transport: http() }); + await publicClient.waitForTransactionReceipt({ hash }); success( "Stablecoin Claimed", diff --git a/examples/nextjs-stablecoin-card-rain/hooks/use-switch-chain.ts b/examples/nextjs-stablecoin-card-rain/hooks/use-switch-chain.ts index 3cdea79..1b33349 100644 --- a/examples/nextjs-stablecoin-card-rain/hooks/use-switch-chain.ts +++ b/examples/nextjs-stablecoin-card-rain/hooks/use-switch-chain.ts @@ -1,5 +1,6 @@ import { useState, useEffect, useRef } from "react"; -import { useDynamicContext } from "@/lib/dynamic"; +import { useWalletAccounts } from "@dynamic-labs-sdk/react-hooks"; +import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm"; export interface UseSwitchChainOptions { targetChainId: string | number; @@ -15,7 +16,8 @@ export function useSwitchChain(options: UseSwitchChainOptions) { onSwitchError, autoSwitch = true, } = options; - const { primaryWallet, network } = useDynamicContext(); + const { walletAccounts } = useWalletAccounts(); + const evmWallet = walletAccounts?.find(isEvmWalletAccount); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -25,40 +27,23 @@ export function useSwitchChain(options: UseSwitchChainOptions) { const hasAttemptedSwitch = useRef(false); const targetChainIdString = String(targetChainId); - const canSwitchNetwork = primaryWallet?.connector.supportsNetworkSwitching(); - const isOnTargetChain = String(network) === targetChainIdString; + // Embedded WaaS wallets manage their network via dashboard configuration; + // network switching is not supported directly via the client SDK. + const canSwitchNetwork = false; + const currentChainId = evmWallet?.network?.id ? String(evmWallet.network.id) : undefined; + const isOnTargetChain = currentChainId === targetChainIdString; const switchChain = async () => { - if (!primaryWallet || !canSwitchNetwork) { - const err = new Error("Wallet does not support network switching"); - setError(err); - onSwitchError?.(err); - return false; - } - if (isOnTargetChain) { setHasSwitched(true); onSwitchSuccess?.(); return true; } - setIsLoading(true); - setError(null); - - try { - await primaryWallet.switchNetwork(targetChainId); - setHasSwitched(true); - onSwitchSuccess?.(); - return true; - } catch (err) { - const error = - err instanceof Error ? err : new Error("Failed to switch chain"); - setError(error); - onSwitchError?.(error); - return false; - } finally { - setIsLoading(false); - } + const err = new Error("Network switching is managed via the Dynamic dashboard for embedded wallets"); + setError(err); + onSwitchError?.(err); + return false; }; // Auto-switch effect that ensures single execution @@ -66,8 +51,7 @@ export function useSwitchChain(options: UseSwitchChainOptions) { if ( autoSwitch && !hasAttemptedSwitch.current && - primaryWallet && - canSwitchNetwork && + evmWallet && !isOnTargetChain && !isLoading && !hasSwitched @@ -76,8 +60,7 @@ export function useSwitchChain(options: UseSwitchChainOptions) { switchChain(); } }, [ - primaryWallet, - canSwitchNetwork, + evmWallet, isOnTargetChain, autoSwitch, isLoading, @@ -86,13 +69,13 @@ export function useSwitchChain(options: UseSwitchChainOptions) { // Reset attempted switch flag if wallet changes useEffect(() => { - if (primaryWallet) { + if (evmWallet) { // Reset the flag when wallet changes to allow switching on new wallet hasAttemptedSwitch.current = false; setHasSwitched(false); setError(null); } - }, [primaryWallet?.address]); + }, [evmWallet?.address]); const resetSwitch = () => { hasAttemptedSwitch.current = false; diff --git a/examples/nextjs-stablecoin-card-rain/hooks/use-withdraw-asset.ts b/examples/nextjs-stablecoin-card-rain/hooks/use-withdraw-asset.ts index c08b184..84c15ed 100644 --- a/examples/nextjs-stablecoin-card-rain/hooks/use-withdraw-asset.ts +++ b/examples/nextjs-stablecoin-card-rain/hooks/use-withdraw-asset.ts @@ -1,11 +1,13 @@ import { useState } from "react"; -import { useDynamicContext } from "@dynamic-labs/sdk-react-core"; -import { isEthereumWallet } from "@dynamic-labs/ethereum"; +import { createPublicClient, http } from "viem"; + +import { useWalletAccounts } from "@dynamic-labs-sdk/react-hooks"; +import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm"; +import { createWalletClientForWalletAccount } from "@dynamic-labs-sdk/evm/viem"; import { useToast } from "@/lib/toast-context"; import { UserWithdrawalRequest } from "@/lib/rain"; import { getAdminSignature } from "./get-admin-signature"; -import { isZeroDevConnector } from "@/lib/dynamic"; import { ADMIN_NONCE_ABI, WITHDRAW_ASSET_ABI } from "@/constants"; export interface UseWithdrawAssetOptions { @@ -14,7 +16,7 @@ export interface UseWithdrawAssetOptions { } export function useWithdrawAsset(options?: UseWithdrawAssetOptions) { - const { primaryWallet, network } = useDynamicContext(); + const { walletAccounts } = useWalletAccounts(); const { success } = useToast(); const [isLoading, setIsLoading] = useState(false); @@ -26,7 +28,8 @@ export function useWithdrawAsset(options?: UseWithdrawAssetOptions) { coordinatorAddress: string, data: UserWithdrawalRequest ) => { - if (!primaryWallet || !isEthereumWallet(primaryWallet)) { + const evmWallet = walletAccounts?.find(isEvmWalletAccount); + if (!evmWallet) { throw new Error("Wallet not connected or not EVM compatible"); } @@ -43,8 +46,8 @@ export function useWithdrawAsset(options?: UseWithdrawAssetOptions) { ] = data.parameters; try { - const walletClient = await primaryWallet.getWalletClient(); - const publicClient = await primaryWallet.getPublicClient(); + const walletClient = await createWalletClientForWalletAccount({ walletAccount: evmWallet }); + const publicClient = createPublicClient({ chain: evmWallet.network, transport: http() }); const nonce = await publicClient.readContract({ address: collateralProxy as `0x${string}`, @@ -52,12 +55,16 @@ export function useWithdrawAsset(options?: UseWithdrawAssetOptions) { functionName: "adminNonce", }); + const chainId = evmWallet.network?.id; + if (!chainId) throw new Error("Network not found"); + // Generate admin signature const { salt: adminSalt, signature: adminSignature } = await getAdminSignature({ - primaryWallet, + walletClient, + signerAddress: evmWallet.address, amount: Number(amountInCents), - chainId: Number(network), + chainId: Number(chainId), collateralProxyAddress: collateralProxy, recipientAddress: recipient, tokenAddress: assetAddress, @@ -91,13 +98,7 @@ export function useWithdrawAsset(options?: UseWithdrawAssetOptions) { setTxHash(hash); // Wait for transaction receipt - const connector = primaryWallet.connector; - if (!connector || !isZeroDevConnector(connector)) { - throw new Error("Connector is not a ZeroDev connector"); - } - const kernelClient = connector.getAccountAbstractionProvider(); - if (!kernelClient) throw new Error("Kernel client not found"); - await kernelClient.waitForUserOperationReceipt({ hash }); + await publicClient.waitForTransactionReceipt({ hash }); success("Withdrawal Confirmed"); if (options?.onWithdrawSuccess) options.onWithdrawSuccess(); diff --git a/examples/nextjs-stablecoin-card-rain/lib/dynamic/index.ts b/examples/nextjs-stablecoin-card-rain/lib/dynamic/index.ts index 82857f2..5db3704 100644 --- a/examples/nextjs-stablecoin-card-rain/lib/dynamic/index.ts +++ b/examples/nextjs-stablecoin-card-rain/lib/dynamic/index.ts @@ -1,8 +1,17 @@ -export * from "@dynamic-labs/sdk-react-core"; -export * from "@dynamic-labs/ethereum"; -export { - ZeroDevSmartWalletConnectors, - isZeroDevConnector, -} from "@dynamic-labs/ethereum-aa"; +import { createDynamicClient, initializeClient, type DynamicClient } from "@dynamic-labs-sdk/client"; +import { addWaasEvmExtension } from "@dynamic-labs-sdk/evm/waas"; -export type { TokenBalance } from "@dynamic-labs/sdk-api-core"; +export const dynamicClient: DynamicClient = createDynamicClient({ + environmentId: process.env.NEXT_PUBLIC_DYNAMIC_ENV_ID!, + autoInitialize: false, + metadata: { name: "Rain Stablecoin Card" }, +}); + +let initialized = false; + +export async function initDynamic(): Promise { + if (initialized) return; + initialized = true; + addWaasEvmExtension(dynamicClient); + await initializeClient(dynamicClient); +} diff --git a/examples/nextjs-stablecoin-card-rain/lib/providers.tsx b/examples/nextjs-stablecoin-card-rain/lib/providers.tsx index fcb0cb0..8257e2a 100644 --- a/examples/nextjs-stablecoin-card-rain/lib/providers.tsx +++ b/examples/nextjs-stablecoin-card-rain/lib/providers.tsx @@ -1,30 +1,53 @@ "use client"; +import { useEffect } from "react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { - DynamicContextProvider, - EthereumWalletConnectors, - DynamicUserProfile, - ZeroDevSmartWalletConnectors, -} from "@/lib/dynamic"; +import { DynamicProvider, useEvent } from "@dynamic-labs-sdk/react-hooks"; +import { completeSocialRedirect, detectSocialRedirectUrl } from "@dynamic-labs-sdk/client"; +import { createWaasWalletAccounts, getChainsMissingWaasWalletAccounts } from "@dynamic-labs-sdk/client/waas"; import { ThemeProvider } from "@/components/theme-provider"; import { ToastProvider } from "@/lib/toast-context"; import { TooltipProvider } from "@/components/ui/tooltip"; -import { redirect } from "next/navigation"; +import { dynamicClient, initDynamic } from "@/lib/dynamic"; -export default function Providers({ children }: { children: React.ReactNode }) { - const environmentId = process.env.NEXT_PUBLIC_DYNAMIC_ENV_ID; - if (!environmentId) { - throw new Error( - "NEXT_PUBLIC_DYNAMIC_ENV_ID is not set. Copy .example.env to .env.local and fill it in." - ); - } +const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false, refetchOnWindowFocus: false } }, +}); + +function DynamicBootstrap() { + useEffect(() => { + let cancelled = false; + initDynamic().then(async () => { + if (cancelled || typeof window === "undefined") return; + try { + const url = new URL(window.location.href); + if (await detectSocialRedirectUrl({ url })) { + await completeSocialRedirect({ url }); + window.history.replaceState({}, "", window.location.pathname); + } + } catch { } + }); + return () => { cancelled = true; }; + }, []); + return null; +} - const queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false, refetchOnWindowFocus: false } }, +function WalletBootstrap() { + useEvent({ + event: "userChanged", + listener: async (user) => { + if (!user) return; + const missing = getChainsMissingWaasWalletAccounts(dynamicClient); + if (missing.length > 0) { + await createWaasWalletAccounts({ chains: missing }, dynamicClient); + } + }, }); + return null; +} +export default function Providers({ children }: { children: React.ReactNode }) { return ( - redirect("/"), - }, - }} - > + + + {children} - - + diff --git a/examples/nextjs-stablecoin-card-rain/package.json b/examples/nextjs-stablecoin-card-rain/package.json index 28431d4..260e00f 100644 --- a/examples/nextjs-stablecoin-card-rain/package.json +++ b/examples/nextjs-stablecoin-card-rain/package.json @@ -9,10 +9,9 @@ "lint": "next lint" }, "dependencies": { - "@dynamic-labs/ethereum": "4.49.0", - "@dynamic-labs/ethereum-aa": "4.49.0", - "@dynamic-labs/sdk-api-core": "0.0.831", - "@dynamic-labs/sdk-react-core": "4.49.0", + "@dynamic-labs-sdk/client": "1.4.0", + "@dynamic-labs-sdk/evm": "1.4.0", + "@dynamic-labs-sdk/react-hooks": "1.4.0", "@hookform/resolvers": "5.2.1", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-label": "2.1.7", diff --git a/examples/nextjs-stablecoin-yield-aave/package.json b/examples/nextjs-stablecoin-yield-aave/package.json index 853370c..c945825 100644 --- a/examples/nextjs-stablecoin-yield-aave/package.json +++ b/examples/nextjs-stablecoin-yield-aave/package.json @@ -14,9 +14,9 @@ }, "dependencies": { "@aave/react": "0.4.0", - "@dynamic-labs-sdk/client": "1.2.1", - "@dynamic-labs-sdk/evm": "1.2.1", - "@dynamic-labs-sdk/react-hooks": "0.26.5", + "@dynamic-labs-sdk/client": "1.4.0", + "@dynamic-labs-sdk/evm": "1.4.0", + "@dynamic-labs-sdk/react-hooks": "1.4.0", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-slot": "1.2.3", "@tanstack/react-query": "5.85.6", diff --git a/examples/nextjs-stablecoin-yield-aave/src/lib/dynamic.ts b/examples/nextjs-stablecoin-yield-aave/src/lib/dynamic.ts index aff994a..f9ed2b9 100644 --- a/examples/nextjs-stablecoin-yield-aave/src/lib/dynamic.ts +++ b/examples/nextjs-stablecoin-yield-aave/src/lib/dynamic.ts @@ -1,14 +1,25 @@ -import { createDynamicClient } from "@dynamic-labs-sdk/client"; -import { addEvmExtension } from "@dynamic-labs-sdk/evm"; +import { + createDynamicClient, + initializeClient, + type DynamicClient, +} from "@dynamic-labs-sdk/client"; +import { addWaasEvmExtension } from "@dynamic-labs-sdk/evm/waas"; -export const dynamicClient = createDynamicClient({ +export const dynamicClient: DynamicClient = createDynamicClient({ environmentId: process.env.NEXT_PUBLIC_DYNAMIC_ENV_ID!, + autoInitialize: false, metadata: { name: "Aave Yield" }, }); -if (typeof window !== "undefined") { - addEvmExtension(); -} +let initialized = false; -// No-op on clients that auto-initialize; called by useAuth on mount. -export async function initDynamic(): Promise {} +/** + * Adds the EVM WaaS extension and initializes the client. + * Safe to call multiple times — initialization runs once. + */ +export async function initDynamic(): Promise { + if (initialized) return; + initialized = true; + addWaasEvmExtension(dynamicClient); + await initializeClient(dynamicClient); +} diff --git a/examples/nextjs-stablecoin-yield-aave/src/lib/providers.tsx b/examples/nextjs-stablecoin-yield-aave/src/lib/providers.tsx index debb4c7..368cb45 100644 --- a/examples/nextjs-stablecoin-yield-aave/src/lib/providers.tsx +++ b/examples/nextjs-stablecoin-yield-aave/src/lib/providers.tsx @@ -10,21 +10,20 @@ import { } from "react"; import { getWalletAccounts, - onEvent, isSignedIn, logout, - detectOAuthRedirect, - completeSocialAuthentication, + detectSocialRedirectUrl, + completeSocialRedirect, getActiveNetworkId, } from "@dynamic-labs-sdk/client"; import { createWaasWalletAccounts } from "@dynamic-labs-sdk/client/waas"; import { isEvmWalletAccount, type EvmWalletAccount } from "@dynamic-labs-sdk/evm"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { DynamicProvider, useUser, useWalletAccounts } from "@dynamic-labs-sdk/react-hooks"; +import { DynamicProvider, useUser, useWalletAccounts, useEvent } from "@dynamic-labs-sdk/react-hooks"; import { AaveProvider } from "@aave/react"; import { base } from "viem/chains"; import { client } from "./aave"; -import { dynamicClient } from "./dynamic"; +import { dynamicClient, initDynamic } from "./dynamic"; interface WalletContextValue { evmAccount: EvmWalletAccount | null; @@ -57,6 +56,24 @@ const queryClient = new QueryClient({ }, }); +function DynamicBootstrap() { + useEffect(() => { + let cancelled = false; + initDynamic().then(async () => { + if (cancelled || typeof window === "undefined") return; + try { + const url = new URL(window.location.href); + if (await detectSocialRedirectUrl({ url })) { + await completeSocialRedirect({ url }); + window.history.replaceState({}, "", window.location.pathname); + } + } catch { /* not a social redirect */ } + }); + return () => { cancelled = true; }; + }, []); + return null; +} + function InnerProviders({ children }: { children: ReactNode }) { const loggedIn = useUser() !== null; const evmAccount = useWalletAccounts().find(isEvmWalletAccount) ?? null; @@ -82,33 +99,7 @@ function InnerProviders({ children }: { children: ReactNode }) { } catch {} }, []); - useEffect(() => { - const unsub = onEvent( - { - event: "walletAccountsChanged", - listener: () => { - void ensureEvmWallet(); - }, - }, - dynamicClient, - ); - return () => unsub?.(); - }, [ensureEvmWallet]); - - useEffect(() => { - const handleOAuthRedirect = async () => { - if (typeof window === "undefined") return; - try { - const url = new URL(window.location.href); - if (await detectOAuthRedirect({ url }, dynamicClient)) { - await completeSocialAuthentication({ url }, dynamicClient); - await ensureEvmWallet(); - window.history.replaceState({}, "", window.location.pathname); - } - } catch {} - }; - handleOAuthRedirect(); - }, [ensureEvmWallet]); + useEvent({ event: "walletAccountsChanged", listener: () => { void ensureEvmWallet(); } }); return ( + {children} ); diff --git a/examples/nextjs-stablecoin-yield-kamino/package.json b/examples/nextjs-stablecoin-yield-kamino/package.json index 44e9783..cec8668 100644 --- a/examples/nextjs-stablecoin-yield-kamino/package.json +++ b/examples/nextjs-stablecoin-yield-kamino/package.json @@ -12,9 +12,9 @@ "clean": "rm -rf .next && rm -rf node_modules/.cache" }, "dependencies": { - "@dynamic-labs-sdk/client": "1.2.1", - "@dynamic-labs-sdk/react-hooks": "0.26.5", - "@dynamic-labs-sdk/solana": "1.2.1", + "@dynamic-labs-sdk/client": "1.4.0", + "@dynamic-labs-sdk/react-hooks": "1.4.0", + "@dynamic-labs-sdk/solana": "1.4.0", "@kamino-finance/farms-sdk": "^3.2.24", "@kamino-finance/klend-sdk": "^7.3.21", "@solana/kit": "^2.3.0", diff --git a/examples/nextjs-stablecoin-yield-kamino/src/lib/dynamic.ts b/examples/nextjs-stablecoin-yield-kamino/src/lib/dynamic.ts index adb8237..77a57ba 100644 --- a/examples/nextjs-stablecoin-yield-kamino/src/lib/dynamic.ts +++ b/examples/nextjs-stablecoin-yield-kamino/src/lib/dynamic.ts @@ -1,20 +1,31 @@ -import { createDynamicClient, getNetworksData } from "@dynamic-labs-sdk/client"; -import { addSolanaExtension } from "@dynamic-labs-sdk/solana"; +import { + createDynamicClient, + initializeClient, + getNetworksData, + type DynamicClient, +} from "@dynamic-labs-sdk/client"; +import { addWaasSolanaExtension } from "@dynamic-labs-sdk/solana/waas"; -// Create the Dynamic client once. Extensions must be registered immediately -// after createDynamicClient() and before initialization completes. -export const dynamicClient = createDynamicClient({ +export const dynamicClient: DynamicClient = createDynamicClient({ environmentId: process.env.NEXT_PUBLIC_DYNAMIC_ENV_ID!, + autoInitialize: false, metadata: { name: "Kamino Earn with Dynamic", }, }); -// Register Solana extension — takes NO arguments -addSolanaExtension(); +let initialized = false; -// No-op on clients that auto-initialize; called by useAuth on mount. -export async function initDynamic(): Promise {} +/** + * Adds the Solana WaaS extension and initializes the client. + * Safe to call multiple times — initialization runs once. + */ +export async function initDynamic(): Promise { + if (initialized) return; + initialized = true; + addWaasSolanaExtension(dynamicClient); + await initializeClient(dynamicClient); +} /** * Returns the Solana RPC URL configured in the Dynamic dashboard. diff --git a/examples/nextjs-stablecoin-yield-kamino/src/lib/providers.tsx b/examples/nextjs-stablecoin-yield-kamino/src/lib/providers.tsx index eb64a80..054281f 100644 --- a/examples/nextjs-stablecoin-yield-kamino/src/lib/providers.tsx +++ b/examples/nextjs-stablecoin-yield-kamino/src/lib/providers.tsx @@ -9,11 +9,10 @@ import { } from "react"; import { getWalletAccounts, - onEvent, isSignedIn, logout, - detectOAuthRedirect, - completeSocialAuthentication, + detectSocialRedirectUrl, + completeSocialRedirect, } from "@dynamic-labs-sdk/client"; import { createWaasWalletAccounts } from "@dynamic-labs-sdk/client/waas"; import { @@ -21,8 +20,8 @@ import { type SolanaWalletAccount, } from "@dynamic-labs-sdk/solana"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { DynamicProvider, useUser, useWalletAccounts } from "@dynamic-labs-sdk/react-hooks"; -import { dynamicClient } from "./dynamic"; +import { DynamicProvider, useUser, useWalletAccounts, useEvent } from "@dynamic-labs-sdk/react-hooks"; +import { dynamicClient, initDynamic } from "./dynamic"; interface WalletContextValue { solanaAccount: SolanaWalletAccount | null; @@ -51,6 +50,24 @@ const queryClient = new QueryClient({ }, }); +function DynamicBootstrap() { + useEffect(() => { + let cancelled = false; + initDynamic().then(async () => { + if (cancelled || typeof window === "undefined") return; + try { + const url = new URL(window.location.href); + if (await detectSocialRedirectUrl({ url })) { + await completeSocialRedirect({ url }); + window.history.replaceState({}, "", window.location.pathname); + } + } catch { /* not a social redirect */ } + }); + return () => { cancelled = true; }; + }, []); + return null; +} + function InnerProviders({ children }: { children: ReactNode }) { const loggedIn = useUser() !== null; const solanaAccount = useWalletAccounts().find(isSolanaWalletAccount) ?? null; @@ -68,33 +85,7 @@ function InnerProviders({ children }: { children: ReactNode }) { } catch {} }, []); - useEffect(() => { - const unsub = onEvent( - { - event: "walletAccountsChanged", - listener: () => { - void ensureSolanaWallet(); - }, - }, - dynamicClient, - ); - return () => unsub?.(); - }, [ensureSolanaWallet]); - - useEffect(() => { - const handleOAuthRedirect = async () => { - if (typeof window === "undefined") return; - try { - const url = new URL(window.location.href); - if (await detectOAuthRedirect({ url }, dynamicClient)) { - await completeSocialAuthentication({ url }, dynamicClient); - await ensureSolanaWallet(); - window.history.replaceState({}, "", window.location.pathname); - } - } catch {} - }; - handleOAuthRedirect(); - }, [ensureSolanaWallet]); + useEvent({ event: "walletAccountsChanged", listener: () => { void ensureSolanaWallet(); } }); return ( + {children} ); diff --git a/examples/nextjs-stablecoin-yield-pods/package.json b/examples/nextjs-stablecoin-yield-pods/package.json index e1f71aa..3117d5e 100644 --- a/examples/nextjs-stablecoin-yield-pods/package.json +++ b/examples/nextjs-stablecoin-yield-pods/package.json @@ -13,9 +13,9 @@ "build:analyze": "ANALYZE=true next build" }, "dependencies": { - "@dynamic-labs-sdk/client": "1.2.1", - "@dynamic-labs-sdk/evm": "1.2.1", - "@dynamic-labs-sdk/react-hooks": "0.26.5", + "@dynamic-labs-sdk/client": "1.4.0", + "@dynamic-labs-sdk/evm": "1.4.0", + "@dynamic-labs-sdk/react-hooks": "1.4.0", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-slot": "1.2.4", "@tanstack/react-query": "5.90.9", diff --git a/examples/nextjs-stablecoin-yield-pods/src/lib/dynamic.ts b/examples/nextjs-stablecoin-yield-pods/src/lib/dynamic.ts index edd19c9..5682188 100644 --- a/examples/nextjs-stablecoin-yield-pods/src/lib/dynamic.ts +++ b/examples/nextjs-stablecoin-yield-pods/src/lib/dynamic.ts @@ -1,14 +1,25 @@ -import { createDynamicClient } from "@dynamic-labs-sdk/client"; -import { addEvmExtension } from "@dynamic-labs-sdk/evm"; +import { + createDynamicClient, + initializeClient, + type DynamicClient, +} from "@dynamic-labs-sdk/client"; +import { addWaasEvmExtension } from "@dynamic-labs-sdk/evm/waas"; -export const dynamicClient = createDynamicClient({ +export const dynamicClient: DynamicClient = createDynamicClient({ environmentId: process.env.NEXT_PUBLIC_DYNAMIC_ENV_ID!, + autoInitialize: false, metadata: { name: "Pods Yield" }, }); -if (typeof window !== "undefined") { - addEvmExtension(); -} +let initialized = false; -// No-op on clients that auto-initialize; called by useAuth on mount. -export async function initDynamic(): Promise {} +/** + * Adds the EVM WaaS extension and initializes the client. + * Safe to call multiple times — initialization runs once. + */ +export async function initDynamic(): Promise { + if (initialized) return; + initialized = true; + addWaasEvmExtension(dynamicClient); + await initializeClient(dynamicClient); +} diff --git a/examples/nextjs-stablecoin-yield-pods/src/lib/providers.tsx b/examples/nextjs-stablecoin-yield-pods/src/lib/providers.tsx index 2ee63a8..a10c0e3 100644 --- a/examples/nextjs-stablecoin-yield-pods/src/lib/providers.tsx +++ b/examples/nextjs-stablecoin-yield-pods/src/lib/providers.tsx @@ -10,18 +10,17 @@ import { } from "react"; import { getWalletAccounts, - onEvent, isSignedIn, logout, - detectOAuthRedirect, - completeSocialAuthentication, + detectSocialRedirectUrl, + completeSocialRedirect, getActiveNetworkId, } from "@dynamic-labs-sdk/client"; import { createWaasWalletAccounts } from "@dynamic-labs-sdk/client/waas"; import { isEvmWalletAccount, type EvmWalletAccount } from "@dynamic-labs-sdk/evm"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { DynamicProvider, useUser, useWalletAccounts } from "@dynamic-labs-sdk/react-hooks"; -import { dynamicClient } from "./dynamic"; +import { DynamicProvider, useUser, useWalletAccounts, useEvent } from "@dynamic-labs-sdk/react-hooks"; +import { dynamicClient, initDynamic } from "./dynamic"; interface WalletContextValue { evmAccount: EvmWalletAccount | null; @@ -54,6 +53,24 @@ const queryClient = new QueryClient({ }, }); +function DynamicBootstrap() { + useEffect(() => { + let cancelled = false; + initDynamic().then(async () => { + if (cancelled || typeof window === "undefined") return; + try { + const url = new URL(window.location.href); + if (await detectSocialRedirectUrl({ url })) { + await completeSocialRedirect({ url }); + window.history.replaceState({}, "", window.location.pathname); + } + } catch { /* not a social redirect */ } + }); + return () => { cancelled = true; }; + }, []); + return null; +} + function InnerProviders({ children }: { children: ReactNode }) { const loggedIn = useUser() !== null; const evmAccount = useWalletAccounts().find(isEvmWalletAccount) ?? null; @@ -79,33 +96,7 @@ function InnerProviders({ children }: { children: ReactNode }) { } catch {} }, []); - useEffect(() => { - const unsub = onEvent( - { - event: "walletAccountsChanged", - listener: () => { - void ensureEvmWallet(); - }, - }, - dynamicClient, - ); - return () => unsub?.(); - }, [ensureEvmWallet]); - - useEffect(() => { - const handleOAuthRedirect = async () => { - if (typeof window === "undefined") return; - try { - const url = new URL(window.location.href); - if (await detectOAuthRedirect({ url }, dynamicClient)) { - await completeSocialAuthentication({ url }, dynamicClient); - await ensureEvmWallet(); - window.history.replaceState({}, "", window.location.pathname); - } - } catch {} - }; - handleOAuthRedirect(); - }, [ensureEvmWallet]); + useEvent({ event: "walletAccountsChanged", listener: () => { void ensureEvmWallet(); } }); return ( + {children} ); diff --git a/examples/vite-linera-counter/package.json b/examples/vite-linera-counter/package.json index 0e1bde6..5e181dc 100644 --- a/examples/vite-linera-counter/package.json +++ b/examples/vite-linera-counter/package.json @@ -11,8 +11,10 @@ }, "dependencies": { "@apollo/client": "^4.0.1", - "@dynamic-labs/ethereum": "^4.32.1", - "@dynamic-labs/sdk-react-core": "^4.32.1", + "@dynamic-labs-sdk/client": "1.4.0", + "@dynamic-labs-sdk/evm": "1.4.0", + "@dynamic-labs-sdk/react-hooks": "1.4.0", + "@tanstack/react-query": "5.100.14", "@linera/client": "file:./linera-protocol/linera-web", "graphql": "^16.11.0", "graphql-ws": "^6.0.6", diff --git a/examples/vite-linera-counter/src/App.tsx b/examples/vite-linera-counter/src/App.tsx index 638f7cb..17d5715 100644 --- a/examples/vite-linera-counter/src/App.tsx +++ b/examples/vite-linera-counter/src/App.tsx @@ -1,11 +1,17 @@ +import { useEffect } from "react"; import WalletControls from "./components/WalletControls"; import { useDarkMode } from "./lib/useDarkMode"; import DynamicMethods from "./components/Methods"; +import { initDynamic } from "./lib/dynamic"; import "./App.css"; function App() { const { isDarkMode } = useDarkMode(); + useEffect(() => { + initDynamic(); + }, []); + return (
diff --git a/examples/vite-linera-counter/src/components/Methods.tsx b/examples/vite-linera-counter/src/components/Methods.tsx index 585b412..dd2ecdb 100644 --- a/examples/vite-linera-counter/src/components/Methods.tsx +++ b/examples/vite-linera-counter/src/components/Methods.tsx @@ -1,5 +1,6 @@ import { useState, useEffect, useRef } from "react"; -import { useDynamicContext, useIsLoggedIn } from "@dynamic-labs/sdk-react-core"; +import { useUser, useWalletAccounts } from "@dynamic-labs-sdk/react-hooks"; +import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm"; import { lineraAdapter, type LineraProvider } from "../lib/linera-adapter"; import "./Methods.css"; @@ -15,8 +16,11 @@ interface Block { } export default function DynamicMethods({ isDarkMode }: DynamicMethodsProps) { - const { sdkHasLoaded, primaryWallet } = useDynamicContext(); - const isLoggedIn = useIsLoggedIn(); + const user = useUser(); + const { walletAccounts } = useWalletAccounts(); + const isLoggedIn = user !== null; + + const primaryWallet = walletAccounts?.find(isEvmWalletAccount); const [isLoading, setIsLoading] = useState(true); const [result, setResult] = useState(""); @@ -32,9 +36,9 @@ export default function DynamicMethods({ isDarkMode }: DynamicMethodsProps) { const [blocks, setBlocks] = useState([]); useEffect(() => { - if (sdkHasLoaded && isLoggedIn && primaryWallet) setIsLoading(false); + if (isLoggedIn && primaryWallet) setIsLoading(false); else setIsLoading(true); - }, [sdkHasLoaded, isLoggedIn, primaryWallet]); + }, [isLoggedIn, primaryWallet]); useEffect(() => { setChainConnected(lineraAdapter.isChainConnected()); diff --git a/examples/vite-linera-counter/src/components/WalletControls.tsx b/examples/vite-linera-counter/src/components/WalletControls.tsx index 1e51a96..8322a72 100644 --- a/examples/vite-linera-counter/src/components/WalletControls.tsx +++ b/examples/vite-linera-counter/src/components/WalletControls.tsx @@ -1,4 +1,7 @@ -import { useDynamicContext, useIsLoggedIn } from "@dynamic-labs/sdk-react-core"; +import { useUser, useWalletAccounts } from "@dynamic-labs-sdk/react-hooks"; +import { logout } from "@dynamic-labs-sdk/client"; +import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm"; +import { dynamicClient } from "../lib/dynamic"; function shortenAddress(address: string): string { if (!address) return ""; @@ -6,12 +9,11 @@ function shortenAddress(address: string): string { } export default function WalletControls() { - const { sdkHasLoaded, primaryWallet, setShowAuthFlow, handleLogOut } = - useDynamicContext(); - const isLoggedIn = useIsLoggedIn(); - - if (!sdkHasLoaded) return null; + const user = useUser(); + const { walletAccounts } = useWalletAccounts(); + const isLoggedIn = user !== null; + const primaryWallet = walletAccounts?.find(isEvmWalletAccount); const address = primaryWallet?.address || ""; return ( @@ -19,12 +21,15 @@ export default function WalletControls() { {isLoggedIn && address ? ( <> {shortenAddress(address)} - ) : ( - )} diff --git a/examples/vite-linera-counter/src/lib/dynamic-signer.ts b/examples/vite-linera-counter/src/lib/dynamic-signer.ts index b5cce19..9416e8f 100644 --- a/examples/vite-linera-counter/src/lib/dynamic-signer.ts +++ b/examples/vite-linera-counter/src/lib/dynamic-signer.ts @@ -1,11 +1,11 @@ import type { Signer } from "@linera/client"; -import type { Wallet as DynamicWallet } from "@dynamic-labs/sdk-react-core"; -import { isEthereumWallet } from "@dynamic-labs/ethereum"; +import type { EvmWalletAccount } from "@dynamic-labs-sdk/evm"; +import { createWalletClientForWalletAccount } from "@dynamic-labs-sdk/evm/viem"; export class DynamicSigner implements Signer { - private dynamicWallet: DynamicWallet; + private dynamicWallet: EvmWalletAccount; - constructor(dynamicWallet: DynamicWallet) { + constructor(dynamicWallet: EvmWalletAccount) { this.dynamicWallet = dynamicWallet; } @@ -38,9 +38,9 @@ export class DynamicSigner implements Signer { // the standard signing flow and use `personal_sign` directly on the wallet client. // DO NOT USE: this.dynamicWallet.signMessage(msgHex) - it would cause double-hashing - // Note: First cast the wallet to an Ethereum wallet to get the wallet client - if (!isEthereumWallet(this.dynamicWallet)) throw new Error(); - const walletClient = await this.dynamicWallet.getWalletClient(); + const walletClient = await createWalletClientForWalletAccount({ + walletAccount: this.dynamicWallet, + }); const signature = await walletClient.request({ method: "personal_sign", params: [msgHex, address], diff --git a/examples/vite-linera-counter/src/lib/dynamic.ts b/examples/vite-linera-counter/src/lib/dynamic.ts new file mode 100644 index 0000000..8e15130 --- /dev/null +++ b/examples/vite-linera-counter/src/lib/dynamic.ts @@ -0,0 +1,17 @@ +import { createDynamicClient, initializeClient, type DynamicClient } from "@dynamic-labs-sdk/client"; +import { addWaasEvmExtension } from "@dynamic-labs-sdk/evm/waas"; + +export const dynamicClient: DynamicClient = createDynamicClient({ + environmentId: import.meta.env.VITE_DYNAMIC_ENVIRONMENT_ID, + autoInitialize: false, + metadata: { name: "Linera Counter" }, +}); + +let initialized = false; + +export async function initDynamic(): Promise { + if (initialized) return; + initialized = true; + addWaasEvmExtension(dynamicClient); + await initializeClient(dynamicClient); +} diff --git a/examples/vite-linera-counter/src/lib/linera-adapter.ts b/examples/vite-linera-counter/src/lib/linera-adapter.ts index 5ce6f4e..7bd75ec 100644 --- a/examples/vite-linera-counter/src/lib/linera-adapter.ts +++ b/examples/vite-linera-counter/src/lib/linera-adapter.ts @@ -4,7 +4,7 @@ import initLinera, { Wallet, Application, } from "@linera/client"; -import type { Wallet as DynamicWallet } from "@dynamic-labs/sdk-react-core"; +import type { EvmWalletAccount } from "@dynamic-labs-sdk/evm"; import { DynamicSigner } from "./dynamic-signer"; import { LINERA_RPC_URL, COUNTER_APP_ID } from "../constants"; @@ -32,7 +32,7 @@ export class LineraAdapter { } async connect( - dynamicWallet: DynamicWallet, + dynamicWallet: EvmWalletAccount, rpcUrl?: string ): Promise { if (this.provider) return this.provider; diff --git a/examples/vite-linera-counter/src/main.tsx b/examples/vite-linera-counter/src/main.tsx index 7473bce..cb351c9 100644 --- a/examples/vite-linera-counter/src/main.tsx +++ b/examples/vite-linera-counter/src/main.tsx @@ -1,21 +1,27 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; -import { EthereumWalletConnectors } from "@dynamic-labs/ethereum"; -import { DynamicContextProvider } from "@dynamic-labs/sdk-react-core"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { DynamicProvider } from "@dynamic-labs-sdk/react-hooks"; +import { dynamicClient } from "./lib/dynamic"; import App from "./App"; import "./index.css"; +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + refetchOnWindowFocus: false, + }, + }, +}); + createRoot(document.getElementById("root")!).render( - - - + + + + + ); diff --git a/examples/vite-stablecoin-payment-links/package.json b/examples/vite-stablecoin-payment-links/package.json index f7cddf7..8996078 100644 --- a/examples/vite-stablecoin-payment-links/package.json +++ b/examples/vite-stablecoin-payment-links/package.json @@ -10,8 +10,10 @@ "preview": "vite preview" }, "dependencies": { - "@dynamic-labs/ethereum": "^4.32.1", - "@dynamic-labs/sdk-react-core": "^4.32.1", + "@dynamic-labs-sdk/client": "1.4.0", + "@dynamic-labs-sdk/evm": "1.4.0", + "@dynamic-labs-sdk/react-hooks": "1.4.0", + "@tanstack/react-query": "5.100.14", "react": "^18", "react-dom": "^18", "viem": "^2.28.0" diff --git a/examples/vite-stablecoin-payment-links/src/App.tsx b/examples/vite-stablecoin-payment-links/src/App.tsx index 4bd8b9a..a52e5d4 100644 --- a/examples/vite-stablecoin-payment-links/src/App.tsx +++ b/examples/vite-stablecoin-payment-links/src/App.tsx @@ -1,6 +1,8 @@ import { useEffect, useState } from "react"; -import { DynamicWidget } from "@dynamic-labs/sdk-react-core"; +import { useUser } from "@dynamic-labs-sdk/react-hooks"; +import { logout } from "@dynamic-labs-sdk/client"; +import { dynamicClient, initDynamic } from "./lib/dynamic"; import { useDarkMode } from "./lib/useDarkMode"; import PaymentLinkGenerator from "./components/PaymentLinkGenerator"; import PaymentProcessor from "./components/PaymentProcessor"; @@ -9,10 +11,27 @@ import Footer from "./components/Footer"; import "./App.css"; +function DynamicButton() { + const user = useUser(); + return user ? ( + + ) : ( + + ); +} + function App() { const { isDarkMode } = useDarkMode(); const [hasPaymentParams, setHasPaymentParams] = useState(false); + useEffect(() => { + initDynamic(); + }, []); + useEffect(() => { // Check if there are payment parameters in the URL const urlParams = new URLSearchParams(window.location.search); @@ -27,7 +46,7 @@ function App() {
- + {!hasPaymentParams && }
diff --git a/examples/vite-stablecoin-payment-links/src/components/PaymentLinkGenerator.tsx b/examples/vite-stablecoin-payment-links/src/components/PaymentLinkGenerator.tsx index 9750089..b79a95f 100644 --- a/examples/vite-stablecoin-payment-links/src/components/PaymentLinkGenerator.tsx +++ b/examples/vite-stablecoin-payment-links/src/components/PaymentLinkGenerator.tsx @@ -1,9 +1,7 @@ import { useState } from "react"; -import { - useDynamicContext, - DynamicConnectButton, -} from "@dynamic-labs/sdk-react-core"; -import { isEthereumWallet } from "@dynamic-labs/ethereum"; +import { useUser, useWalletAccounts } from "@dynamic-labs-sdk/react-hooks"; +import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm"; +import { dynamicClient } from "../lib/dynamic"; import "./PaymentLinkGenerator.css"; export default function PaymentLinkGenerator({ @@ -16,25 +14,25 @@ export default function PaymentLinkGenerator({ const [reference, setReference] = useState(""); const [paymentLink, setPaymentLink] = useState(""); const [copied, setCopied] = useState(false); - const { primaryWallet } = useDynamicContext(); + const user = useUser(); + const { walletAccounts } = useWalletAccounts(); + const evmWallet = walletAccounts?.find(isEvmWalletAccount); const generatePaymentLink = async () => { - if (!primaryWallet?.address || !isEthereumWallet(primaryWallet)) { + if (!evmWallet?.address) { alert("Please connect an Ethereum wallet to generate payment links"); return; } try { - // Get current network - const currentChainId = await primaryWallet.connector.getNetwork(); - // Create a payment link with preset parameters + // Base Sepolia is the configured network (set in Dynamic dashboard) const baseUrl = window.location.origin; const params = new URLSearchParams({ - recipient: primaryWallet.address, + recipient: evmWallet.address, amount: amount, token: "USDC", - network: currentChainId?.toString() || "84532", // Default to Base Sepolia + network: "84532", // Base Sepolia ...(description && { description }), ...(reference && { reference }), timestamp: Date.now().toString(), @@ -115,15 +113,18 @@ export default function PaymentLinkGenerator({
- {!primaryWallet ? ( - + {!user ? ( + ) : ( @@ -170,9 +171,9 @@ export default function PaymentLinkGenerator({ )}

Recipient:{" "} - {primaryWallet?.address?.substring(0, 8)}... - {primaryWallet?.address?.substring( - primaryWallet.address.length - 6 + {evmWallet?.address?.substring(0, 8)}... + {evmWallet?.address?.substring( + evmWallet.address.length - 6 )}

diff --git a/examples/vite-stablecoin-payment-links/src/components/PaymentProcessor.tsx b/examples/vite-stablecoin-payment-links/src/components/PaymentProcessor.tsx index 82180bc..da8ba55 100644 --- a/examples/vite-stablecoin-payment-links/src/components/PaymentProcessor.tsx +++ b/examples/vite-stablecoin-payment-links/src/components/PaymentProcessor.tsx @@ -1,6 +1,8 @@ import { useEffect, useState } from "react"; -import { useDynamicContext } from "@dynamic-labs/sdk-react-core"; -import { isEthereumWallet } from "@dynamic-labs/ethereum"; +import { useUser, useWalletAccounts } from "@dynamic-labs-sdk/react-hooks"; +import { isEvmWalletAccount } from "@dynamic-labs-sdk/evm"; +import { createWalletClientForWalletAccount } from "@dynamic-labs-sdk/evm/viem"; +import { baseSepolia } from "viem/chains"; import { parseUnits, erc20Abi } from "viem"; import "./PaymentProcessor.css"; @@ -23,7 +25,9 @@ export default function PaymentProcessor({ isDarkMode }: Props) { const [error, setError] = useState(null); const [success, setSuccess] = useState(false); const [paymentData, setPaymentData] = useState(null); - const { primaryWallet, user } = useDynamicContext(); + const user = useUser(); + const { walletAccounts } = useWalletAccounts(); + const evmWallet = walletAccounts?.find(isEvmWalletAccount); // Extract payment parameters from URL useEffect(() => { @@ -61,7 +65,7 @@ export default function PaymentProcessor({ isDarkMode }: Props) { return; } - if (!primaryWallet || !isEthereumWallet(primaryWallet)) { + if (!evmWallet) { setError("Wallet not connected or not EVM compatible"); return; } @@ -70,69 +74,12 @@ export default function PaymentProcessor({ isDarkMode }: Props) { setError(null); try { - // Get enabled networks to check if Base Sepolia is available - const enabledNetworks = primaryWallet.connector.getEnabledNetworks(); - console.log("enabledNetworks", enabledNetworks); - const baseSepoliaNetwork = enabledNetworks.find( - (network) => network.chainId === 84532 - ); - - if (!baseSepoliaNetwork) { - setError( - "Base Sepolia network is not available in your wallet. Please add Base Sepolia network to your wallet." - ); - return; - } - - // Get current network - const currentChainId = await primaryWallet.connector.getNetwork(); - - console.log("currentChainId", currentChainId); - - // Check if wallet is on Base Sepolia - if (currentChainId !== 84532) { - // Try to switch to Base Sepolia - if (primaryWallet.connector.supportsNetworkSwitching()) { - try { - setError("Switching to Base Sepolia Testnet..."); - await primaryWallet.switchNetwork(84532); - // Verify the switch was successful - const newChainId = await primaryWallet.connector.getNetwork(); - if (newChainId !== 84532) { - setError( - "Failed to switch to Base Sepolia Testnet. Please switch manually." - ); - return; - } - } catch (switchError: unknown) { - setError( - `Failed to switch network: ${ - switchError instanceof Error - ? switchError.message - : "Unknown error" - }. Please switch to Base Sepolia Testnet manually.` - ); - return; - } - } else { - setError( - "Please switch to Base Sepolia Testnet. Your wallet doesn't support automatic network switching." - ); - return; - } - } - - // Double-check we're on Base Sepolia before proceeding - const finalChainId = await primaryWallet.connector.getNetwork(); - if (finalChainId !== 84532) { - setError( - `Wallet is on wrong network. Expected Base Sepolia (84532), got ${finalChainId}` - ); - return; - } - // Get wallet client for Base Sepolia - const walletClient = await primaryWallet.getWalletClient(); + // Embedded wallets handle network configuration via the Dynamic dashboard + const walletClient = await createWalletClientForWalletAccount({ + walletAccount: evmWallet, + chain: baseSepolia, + }); // Use Base Sepolia USDC contract address const usdcAddress = "0x036CbD53842c5426634e7929541eC2318f3dCF7e"; diff --git a/examples/vite-stablecoin-payment-links/src/lib/dynamic.ts b/examples/vite-stablecoin-payment-links/src/lib/dynamic.ts new file mode 100644 index 0000000..c619335 --- /dev/null +++ b/examples/vite-stablecoin-payment-links/src/lib/dynamic.ts @@ -0,0 +1,17 @@ +import { createDynamicClient, initializeClient, type DynamicClient } from "@dynamic-labs-sdk/client"; +import { addWaasEvmExtension } from "@dynamic-labs-sdk/evm/waas"; + +export const dynamicClient: DynamicClient = createDynamicClient({ + environmentId: import.meta.env.VITE_DYNAMIC_ENVIRONMENT_ID, + autoInitialize: false, + metadata: { name: "Stablecoin Payment Links" }, +}); + +let initialized = false; + +export async function initDynamic(): Promise { + if (initialized) return; + initialized = true; + addWaasEvmExtension(dynamicClient); + await initializeClient(dynamicClient); +} diff --git a/examples/vite-stablecoin-payment-links/src/main.tsx b/examples/vite-stablecoin-payment-links/src/main.tsx index 3f58135..36cb51f 100644 --- a/examples/vite-stablecoin-payment-links/src/main.tsx +++ b/examples/vite-stablecoin-payment-links/src/main.tsx @@ -1,46 +1,26 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; -import { EthereumWalletConnectors } from "@dynamic-labs/ethereum"; -import { DynamicContextProvider } from "@dynamic-labs/sdk-react-core"; - +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { DynamicProvider } from "@dynamic-labs-sdk/react-hooks"; +import { dynamicClient } from "./lib/dynamic"; import App from "./App"; import "./index.css"; -const NETWORK_OVERRIDES = [ - { - blockExplorerUrls: ['https://sepolia.basescan.org/'], - chainId: 84532, - chainName: 'Base Sepolia', - iconUrls: [ - 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599/logo.png', - ], - name: 'Base Sepolia Testnet', - nativeCurrency: { - decimals: 18, - name: 'Ether', - symbol: 'ETH', - iconUrl: - 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599/logo.png', +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + refetchOnWindowFocus: false, }, - networkId: 84532, - rpcUrls: ['https://sepolia.base.org'], - vanityName: 'Base Sepolia Testnet', }, -]; +}); createRoot(document.getElementById("root")!).render( - - - + + + + + -); \ No newline at end of file +);