diff --git a/account-kit/core/src/store/store.test.ts b/account-kit/core/src/store/store.test.ts index b4148948e6..bd894f55ad 100644 --- a/account-kit/core/src/store/store.test.ts +++ b/account-kit/core/src/store/store.test.ts @@ -218,7 +218,157 @@ describe("createConfig tests", () => { "421614": {}, }, }, - "version": 8, + "version": 9, + } + `); + }); + + it("should serialize/deserialize state correctly when using single chain config", async () => { + const config = createConfig({ + chain: sepolia, + transport: alchemy({ rpcUrl: "/api/sepolia" }), + signerConnection: { rpcUrl: "/api/signer" }, + sessionConfig: { + expirationTimeMs: 1000, + }, + policyId: "test-policy-id", + storage: () => localStorage, + }); + + await config.store.persist.rehydrate(); + + config.store.setState({ + accounts: createDefaultAccountState([sepolia]), + }); + + expect(JSON.parse(localStorage.getItem(DEFAULT_STORAGE_KEY) ?? "{}")) + .toMatchInlineSnapshot(` + { + "state": { + "accountConfigs": { + "11155111": {}, + }, + "chain": { + "blockExplorers": { + "default": { + "apiUrl": "https://api-sepolia.etherscan.io/api", + "name": "Etherscan", + "url": "https://sepolia.etherscan.io", + }, + }, + "contracts": { + "ensRegistry": { + "address": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e", + }, + "ensUniversalResolver": { + "address": "0xc8Af999e38273D658BE1b921b88A9Ddf005769cC", + "blockCreated": 5317080, + }, + "multicall3": { + "address": "0xca11bde05977b3631167028862be2a173976ca11", + "blockCreated": 751532, + }, + }, + "id": 11155111, + "name": "Sepolia", + "nativeCurrency": { + "decimals": 18, + "name": "Sepolia Ether", + "symbol": "ETH", + }, + "rpcUrls": { + "alchemy": { + "http": [ + "https://eth-sepolia.g.alchemy.com/v2", + ], + }, + "default": { + "http": [ + "https://rpc.sepolia.org", + ], + }, + }, + "testnet": true, + }, + "config": { + "client": { + "connection": { + "rpcUrl": "/api/signer", + }, + }, + "sessionConfig": { + "expirationTimeMs": 1000, + }, + }, + "connections": { + "__type": "Map", + "value": [ + [ + 11155111, + { + "chain": { + "blockExplorers": { + "default": { + "apiUrl": "https://api-sepolia.etherscan.io/api", + "name": "Etherscan", + "url": "https://sepolia.etherscan.io", + }, + }, + "contracts": { + "ensRegistry": { + "address": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e", + }, + "ensUniversalResolver": { + "address": "0xc8Af999e38273D658BE1b921b88A9Ddf005769cC", + "blockCreated": 5317080, + }, + "multicall3": { + "address": "0xca11bde05977b3631167028862be2a173976ca11", + "blockCreated": 751532, + }, + }, + "id": 11155111, + "name": "Sepolia", + "nativeCurrency": { + "decimals": 18, + "name": "Sepolia Ether", + "symbol": "ETH", + }, + "rpcUrls": { + "alchemy": { + "http": [ + "https://eth-sepolia.g.alchemy.com/v2", + ], + }, + "default": { + "http": [ + "https://rpc.sepolia.org", + ], + }, + }, + "testnet": true, + }, + "policyId": "test-policy-id", + "transport": { + "__type": "Transport", + "rpcUrl": "/api/sepolia", + }, + }, + ], + ], + }, + "signerStatus": { + "isAuthenticating": false, + "isConnected": false, + "isDisconnected": false, + "isInitializing": true, + "status": "INITIALIZING", + }, + "smartAccountClients": { + "11155111": {}, + }, + }, + "version": 9, } `); }); diff --git a/account-kit/core/src/store/store.ts b/account-kit/core/src/store/store.ts index 2b2d22395d..c55f576015 100644 --- a/account-kit/core/src/store/store.ts +++ b/account-kit/core/src/store/store.ts @@ -110,7 +110,7 @@ export const createAccountKitStore = ( skipHydration: ssr, partialize: ({ signer, accounts, ...writeableState }) => writeableState, - version: 8, + version: 9, }) : () => createInitialStoreState(params) ) diff --git a/account-kit/react/src/components/dialog/dialog.tsx b/account-kit/react/src/components/dialog/dialog.tsx index 32561bde20..42e4e477a8 100644 --- a/account-kit/react/src/components/dialog/dialog.tsx +++ b/account-kit/react/src/components/dialog/dialog.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useCallback, useEffect, useState, type ReactNode } from "react"; import { createPortal } from "react-dom"; import { RemoveScroll } from "react-remove-scroll"; diff --git a/examples/ui-demo/src/app/config.tsx b/examples/ui-demo/src/app/config.tsx new file mode 100644 index 0000000000..b01bf7c77b --- /dev/null +++ b/examples/ui-demo/src/app/config.tsx @@ -0,0 +1,82 @@ +import { AuthCardHeader } from "@/components/shared/AuthCardHeader"; +import { alchemy, arbitrumSepolia } from "@account-kit/infra"; +import { cookieStorage, createConfig } from "@account-kit/react"; +import { AccountKitTheme } from "@account-kit/react/tailwind"; +import { QueryClient } from "@tanstack/react-query"; + +export type Config = { + auth: { + showEmail: boolean; + showExternalWallets: boolean; + showPasskey: boolean; + addPasskey: boolean; + }; + ui: { + theme: "light" | "dark"; + primaryColor: { + dark: string; + light: string; + }; + borderRadius: AccountKitTheme["borderRadius"]; + illustrationStyle: "outline" | "linear" | "filled" | "flat"; + logoLight: + | { + fileName: string; + fileSrc: string; + } + | undefined; + logoDark: + | { + fileName: string; + fileSrc: string; + } + | undefined; + }; + supportUrl?: string; +}; + +export const DEFAULT_CONFIG: Config = { + auth: { + showEmail: true, + showExternalWallets: false, + showPasskey: true, + addPasskey: true, + }, + ui: { + theme: "light", + primaryColor: { + light: "#363FF9", + dark: "#9AB7FF", + }, + borderRadius: "sm", + illustrationStyle: "outline", + logoLight: undefined, + logoDark: undefined, + }, +}; + +export const queryClient = new QueryClient(); + +export const alchemyConfig = createConfig( + { + transport: alchemy({ rpcUrl: "/api/rpc" }), + chain: arbitrumSepolia, + ssr: true, + policyId: process.env.NEXT_PUBLIC_PAYMASTER_POLICY_ID, + storage: cookieStorage, + }, + { + illustrationStyle: DEFAULT_CONFIG.ui.illustrationStyle, + auth: { + sections: [[{ type: "email" as const }], [{ type: "passkey" as const }]], + addPasskeyOnSignup: DEFAULT_CONFIG.auth.addPasskey, + header: ( + <AuthCardHeader + theme={DEFAULT_CONFIG.ui.theme} + logoDark={DEFAULT_CONFIG.ui.logoDark} + logoLight={DEFAULT_CONFIG.ui.logoLight} + /> + ), + }, + } +); diff --git a/examples/ui-demo/src/app/layout.tsx b/examples/ui-demo/src/app/layout.tsx index c6bf51eda9..23e7d9f1b3 100644 --- a/examples/ui-demo/src/app/layout.tsx +++ b/examples/ui-demo/src/app/layout.tsx @@ -1,5 +1,8 @@ +import { cookieToInitialState } from "@account-kit/core"; import type { Metadata } from "next"; import { Inter } from "next/font/google"; +import { headers } from "next/headers"; +import { alchemyConfig } from "./config"; import "./globals.css"; import { Providers } from "./providers"; @@ -15,10 +18,15 @@ export default function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { + const initialState = cookieToInitialState( + alchemyConfig, + headers().get("cookie") ?? undefined + ); + return ( <html lang="en" className="light"> <body className={inter.className}> - <Providers>{children}</Providers> + <Providers initialState={initialState}>{children}</Providers> </body> </html> ); diff --git a/examples/ui-demo/src/app/page.tsx b/examples/ui-demo/src/app/page.tsx index b74cd9a6ba..5078f7ef5e 100644 --- a/examples/ui-demo/src/app/page.tsx +++ b/examples/ui-demo/src/app/page.tsx @@ -1,22 +1,22 @@ "use client"; +import { useConfig } from "@/app/state"; import { Authentication } from "@/components/configuration/Authentication"; import { Styling } from "@/components/configuration/Styling"; +import { MobileSplashPage } from "@/components/preview/MobileSplashPage"; +import { + EOAPostLoginActions, + EOAPostLoginContents, +} from "@/components/shared/eoa-post-login/EOAPostLoginContents"; +import { RenderUserConnectionAvatar } from "@/components/shared/user-connection-avatar/RenderUserConnectionAvatar"; +import { cn } from "@/lib/utils"; +import { useUser } from "@account-kit/react"; import { Inter, Public_Sans } from "next/font/google"; import { useState } from "react"; import { AuthCardWrapper } from "../components/preview/AuthCardWrapper"; import { CodePreview } from "../components/preview/CodePreview"; import { CodePreviewSwitch } from "../components/shared/CodePreviewSwitch"; import { TopNav } from "../components/topnav/TopNav"; -import { RenderUserConnectionAvatar } from "@/components/shared/user-connection-avatar/RenderUserConnectionAvatar"; -import { useUser } from "@account-kit/react"; -import { MobileSplashPage } from "@/components/preview/MobileSplashPage"; -import { cn } from "@/lib/utils"; -import { useConfig } from "@/app/state"; -import { - EOAPostLoginContents, - EOAPostLoginActions, -} from "@/components/shared/eoa-post-login/EOAPostLoginContents"; const publicSans = Public_Sans({ subsets: ["latin"], diff --git a/examples/ui-demo/src/app/providers.tsx b/examples/ui-demo/src/app/providers.tsx index 6b98fe713b..c3e3965f1c 100644 --- a/examples/ui-demo/src/app/providers.tsx +++ b/examples/ui-demo/src/app/providers.tsx @@ -1,45 +1,23 @@ "use client"; -import { AuthCardHeader } from "@/components/shared/AuthCardHeader"; -import { alchemy, arbitrumSepolia } from "@account-kit/infra"; -import { AlchemyAccountProvider, createConfig } from "@account-kit/react"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { PropsWithChildren, Suspense } from "react"; -import { ConfigContextProvider, DEFAULT_CONFIG } from "./state"; import { ToastProvider } from "@/contexts/ToastProvider"; +import { AlchemyClientState } from "@account-kit/core"; +import { AlchemyAccountProvider } from "@account-kit/react"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { PropsWithChildren, Suspense } from "react"; +import { alchemyConfig, queryClient } from "./config"; +import { ConfigContextProvider } from "./state"; -const queryClient = new QueryClient(); - -const alchemyConfig = createConfig( - { - transport: alchemy({ rpcUrl: "/api/rpc" }), - chain: arbitrumSepolia, - ssr: true, - policyId: process.env.NEXT_PUBLIC_PAYMASTER_POLICY_ID, - }, - { - illustrationStyle: DEFAULT_CONFIG.ui.illustrationStyle, - auth: { - sections: [[{ type: "email" as const }], [{ type: "passkey" as const }]], - addPasskeyOnSignup: DEFAULT_CONFIG.auth.addPasskey, - header: ( - <AuthCardHeader - theme={DEFAULT_CONFIG.ui.theme} - logoDark={DEFAULT_CONFIG.ui.logoDark} - logoLight={DEFAULT_CONFIG.ui.logoLight} - /> - ), - }, - } -); - -export const Providers = (props: PropsWithChildren<{}>) => { +export const Providers = ( + props: PropsWithChildren<{ initialState?: AlchemyClientState }> +) => { return ( <Suspense> <QueryClientProvider client={queryClient}> <AlchemyAccountProvider config={alchemyConfig} queryClient={queryClient} + initialState={props.initialState} > <ToastProvider> <ConfigContextProvider>{props.children}</ConfigContextProvider> diff --git a/examples/ui-demo/src/app/state.tsx b/examples/ui-demo/src/app/state.tsx index 51859fb2df..05b4f3bdca 100644 --- a/examples/ui-demo/src/app/state.tsx +++ b/examples/ui-demo/src/app/state.tsx @@ -1,3 +1,5 @@ +"use client"; + import { AuthCardHeader } from "@/components/shared/AuthCardHeader"; import { AlchemyAccountsUIConfig, @@ -5,7 +7,6 @@ import { useUiConfig, } from "@account-kit/react"; import { - AccountKitTheme, getBorderRadiusBaseVariableName, getBorderRadiusValue, getColorVariableName, @@ -19,37 +20,7 @@ import { useEffect, useState, } from "react"; - -export type Config = { - auth: { - showEmail: boolean; - showExternalWallets: boolean; - showPasskey: boolean; - addPasskey: boolean; - }; - ui: { - theme: "light" | "dark"; - primaryColor: { - dark: string; - light: string; - }; - borderRadius: AccountKitTheme["borderRadius"]; - illustrationStyle: "outline" | "linear" | "filled" | "flat"; - logoLight: - | { - fileName: string; - fileSrc: string; - } - | undefined; - logoDark: - | { - fileName: string; - fileSrc: string; - } - | undefined; - }; - supportUrl?: string; -}; +import { Config, DEFAULT_CONFIG } from "./config"; export type ConfigContextType = { config: Config; @@ -58,26 +29,6 @@ export type ConfigContextType = { setNFTTransfered: Dispatch<SetStateAction<boolean>>; }; -export const DEFAULT_CONFIG: Config = { - auth: { - showEmail: true, - showExternalWallets: false, - showPasskey: true, - addPasskey: true, - }, - ui: { - theme: "light", - primaryColor: { - light: "#363FF9", - dark: "#9AB7FF", - }, - borderRadius: "sm", - illustrationStyle: "outline", - logoLight: undefined, - logoDark: undefined, - }, -}; - export const ConfigContext = createContext<ConfigContextType>({ config: DEFAULT_CONFIG, setConfig: () => undefined, diff --git a/examples/ui-demo/src/components/preview/AuthCardWrapper.tsx b/examples/ui-demo/src/components/preview/AuthCardWrapper.tsx index 234549ddd2..37b10cc3a6 100644 --- a/examples/ui-demo/src/components/preview/AuthCardWrapper.tsx +++ b/examples/ui-demo/src/components/preview/AuthCardWrapper.tsx @@ -1,9 +1,10 @@ +"use client"; + import { useConfig } from "@/app/state"; import { cn } from "@/lib/utils"; -import { AuthCard, useSmartAccountClient, useUser } from "@account-kit/react"; -import { MintCard } from "../shared/MintCard"; -import { LoadingIcon } from "../icons/loading"; +import { AuthCard, useUser } from "@account-kit/react"; import { EOAPostLogin } from "../shared/eoa-post-login/EOAPostLogin"; +import { MintCard } from "../shared/MintCard"; export function AuthCardWrapper({ className }: { className?: string }) { const { config } = useConfig(); @@ -25,10 +26,7 @@ export function AuthCardWrapper({ className }: { className?: string }) { const RenderContent = () => { const user = useUser(); - const { client } = useSmartAccountClient({ type: "LightAccount" }); - const hasUser = !!user; - const hasClient = !!client; if (!hasUser) { return ( @@ -42,14 +40,6 @@ const RenderContent = () => { const isEOAUser = user.type === "eoa"; - if (hasClient) { - return ( - <div className="py-14 pt-20"> - <MintCard /> - </div> - ); - } - if (isEOAUser) { return ( <div className="py-14 pt-20 h-full lg:h-auto"> @@ -58,5 +48,9 @@ const RenderContent = () => { ); } - return <LoadingIcon />; + return ( + <div className="py-14 pt-20"> + <MintCard /> + </div> + ); }; diff --git a/examples/ui-demo/src/components/preview/CodePreview.tsx b/examples/ui-demo/src/components/preview/CodePreview.tsx index d503646e9e..4164c4b863 100644 --- a/examples/ui-demo/src/components/preview/CodePreview.tsx +++ b/examples/ui-demo/src/components/preview/CodePreview.tsx @@ -1,4 +1,5 @@ -import { Config, DEFAULT_CONFIG, useConfig } from "@/app/state"; +import { Config, DEFAULT_CONFIG } from "@/app/config"; +import { useConfig } from "@/app/state"; import dedent from "dedent"; import { Check, Copy } from "lucide-react"; import { useState } from "react"; @@ -141,7 +142,7 @@ function getConfigCode(config: Config) { return dedent` import { AlchemyAccountsUIConfig, createConfig } from "@account-kit/react"; - import { sepolia } from "@account-kit/infra"; + import { sepolia, alchemy } from "@account-kit/infra"; import { QueryClient } from "@tanstack/react-query"; const uiConfig: AlchemyAccountsUIConfig = { @@ -161,7 +162,7 @@ function getConfigCode(config: Config) { export const config = createConfig({ // if you don't want to leak api keys, you can proxy to a backend and set the rpcUrl instead here // get this from the app config you create at https://dashboard.alchemy.com/accounts - apiKey: "your-api-key", + transport: alchemy({ apiKey: "your-api-key" }), chain: sepolia, ssr: true, // set to false if you're not using server-side rendering }, uiConfig); diff --git a/examples/ui-demo/src/components/shared/AuthCardHeader.tsx b/examples/ui-demo/src/components/shared/AuthCardHeader.tsx index 752f101869..7c8c5b6bf2 100644 --- a/examples/ui-demo/src/components/shared/AuthCardHeader.tsx +++ b/examples/ui-demo/src/components/shared/AuthCardHeader.tsx @@ -1,4 +1,4 @@ -import { Config } from "@/app/state"; +import { Config } from "@/app/config"; export function AuthCardHeader({ logoDark, diff --git a/examples/ui-demo/src/components/shared/MintCard.tsx b/examples/ui-demo/src/components/shared/MintCard.tsx index c747a95c62..f3b81ff11c 100644 --- a/examples/ui-demo/src/components/shared/MintCard.tsx +++ b/examples/ui-demo/src/components/shared/MintCard.tsx @@ -1,21 +1,21 @@ "use client"; -import Image from "next/image"; -import { CheckIcon } from "../icons/check"; -import { GasIcon } from "../icons/gas"; -import { DrawIcon } from "../icons/draw"; -import { ReceiptIcon } from "../icons/receipt"; -import React, { useCallback, useState } from "react"; -import { LoadingIcon } from "../icons/loading"; -import { ExternalLinkIcon } from "../icons/external-link"; +import { useConfig } from "@/app/state"; +import { useToast } from "@/hooks/useToast"; +import { AccountKitNftMinterABI, nftContractAddress } from "@/utils/config"; import { useSendUserOperation, useSmartAccountClient, } from "@account-kit/react"; -import { AccountKitNftMinterABI, nftContractAddress } from "@/utils/config"; -import { encodeFunctionData } from "viem"; -import { useConfig } from "@/app/state"; import { useQuery } from "@tanstack/react-query"; -import { useToast } from "@/hooks/useToast"; +import Image from "next/image"; +import { useCallback, useState } from "react"; +import { encodeFunctionData } from "viem"; +import { CheckIcon } from "../icons/check"; +import { DrawIcon } from "../icons/draw"; +import { ExternalLinkIcon } from "../icons/external-link"; +import { GasIcon } from "../icons/gas"; +import { LoadingIcon } from "../icons/loading"; +import { ReceiptIcon } from "../icons/receipt"; type NFTLoadingState = "loading" | "success"; @@ -60,7 +60,9 @@ export const MintCard = () => { }); }; - const { client } = useSmartAccountClient({ type: "LightAccount" }); + const { client, isLoadingClient } = useSmartAccountClient({ + type: "LightAccount", + }); const { sendUserOperationResult, sendUserOperation } = useSendUserOperation({ client, waitForTxn: true, @@ -196,7 +198,10 @@ export const MintCard = () => { {!nftTransfered ? ( <button className="btn btn-primary w-full p-2 radius mb-4" - disabled={Object.values(status).some((x) => x === "loading")} + disabled={ + Object.values(status).some((x) => x === "loading") || + isLoadingClient + } onClick={handleCollectNFT} > Collect NFT