diff --git a/examples/ui-demo/next.config.mjs b/examples/ui-demo/next.config.mjs index e2ef99ed5..b7790b93b 100644 --- a/examples/ui-demo/next.config.mjs +++ b/examples/ui-demo/next.config.mjs @@ -1,6 +1,16 @@ await import("./env.mjs"); -/** @type {import('next').NextConfig} */ -const nextConfig = {}; +const nextConfig = { + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'static.alchemyapi.io', + port: '', + pathname: '/assets/accountkit/**', + }, + ], + } +}; export default nextConfig; diff --git a/examples/ui-demo/src/app/page.tsx b/examples/ui-demo/src/app/page.tsx index 692a142d8..3299af1af 100644 --- a/examples/ui-demo/src/app/page.tsx +++ b/examples/ui-demo/src/app/page.tsx @@ -3,11 +3,14 @@ import { Authentication } from "@/components/configuration/Authentication"; import { Styling } from "@/components/configuration/Styling"; import { Inter, Public_Sans } from "next/font/google"; -import { useState } from "react"; +import { useContext, 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 { UserConnectionAvatarWithPopover } from "@/components/shared/user-connection-avatar/UserConnectionAvatarWithPopover"; +import { useUser } from "@account-kit/react"; +import { useConfig } from "./state"; const publicSans = Public_Sans({ subsets: ["latin"], @@ -21,7 +24,8 @@ const inter = Inter({ export default function Home() { const [showCode, setShowCode] = useState(false); - + const { nftTransfered } = useConfig(); + const user = useUser(); return (
{/* Code toggle header */} -
-
- Code preview +
+ {!showCode && user && ( + + )} +
+
+ Code preview +
+
-
{/* Don't unmount when showing code preview so that the auth card retains its state */} - - {showCode && } + + {showCode && }
diff --git a/examples/ui-demo/src/app/providers.tsx b/examples/ui-demo/src/app/providers.tsx index 8a0c8932f..5fb4d328e 100644 --- a/examples/ui-demo/src/app/providers.tsx +++ b/examples/ui-demo/src/app/providers.tsx @@ -1,7 +1,7 @@ "use client"; import { AuthCardHeader } from "@/components/shared/AuthCardHeader"; -import { sepolia } from "@account-kit/infra"; +import { arbitrumSepolia } from "@account-kit/infra"; import { AlchemyAccountProvider, createConfig } from "@account-kit/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { PropsWithChildren, Suspense } from "react"; @@ -12,8 +12,9 @@ const queryClient = new QueryClient(); const alchemyConfig = createConfig( { rpcUrl: "/api/rpc", - chain: sepolia, + chain: arbitrumSepolia, ssr: true, + policyId: process.env.NEXT_PUBLIC_PAYMASTER_POLICY_ID }, { illustrationStyle: DEFAULT_CONFIG.ui.illustrationStyle, diff --git a/examples/ui-demo/src/app/state.tsx b/examples/ui-demo/src/app/state.tsx index 48ac3ae66..855e1a552 100644 --- a/examples/ui-demo/src/app/state.tsx +++ b/examples/ui-demo/src/app/state.tsx @@ -53,6 +53,8 @@ export type Config = { export type ConfigContextType = { config: Config; setConfig: Dispatch>; + nftTransfered: boolean; + setNFTTransfered: Dispatch>; }; export const DEFAULT_CONFIG: Config = { @@ -78,6 +80,8 @@ export const DEFAULT_CONFIG: Config = { export const ConfigContext = createContext({ config: DEFAULT_CONFIG, setConfig: () => undefined, + nftTransfered: false, + setNFTTransfered: () => undefined, }); export function useConfig(): ConfigContextType { @@ -88,6 +92,7 @@ export function useConfig(): ConfigContextType { export function ConfigContextProvider(props: PropsWithChildren) { const [config, setConfig] = useState(DEFAULT_CONFIG); + const [nftTransfered, setNFTTransfered] = useState(false); const { updateConfig } = useUiConfig(); // Sync Alchemy auth UI config @@ -151,7 +156,9 @@ export function ConfigContextProvider(props: PropsWithChildren) { }, [config]); return ( - + {props.children} ); diff --git a/examples/ui-demo/src/components/icons/check.tsx b/examples/ui-demo/src/components/icons/check.tsx new file mode 100644 index 000000000..6599b74d2 --- /dev/null +++ b/examples/ui-demo/src/components/icons/check.tsx @@ -0,0 +1,28 @@ +import { SVGProps } from "react"; + +export const CheckIcon = ({ + stroke = "currentColor", + ...props +}: JSX.IntrinsicAttributes & SVGProps) => { + return ( + + + + + + ); +}; diff --git a/examples/ui-demo/src/components/icons/draw.tsx b/examples/ui-demo/src/components/icons/draw.tsx new file mode 100644 index 000000000..daa9e4346 --- /dev/null +++ b/examples/ui-demo/src/components/icons/draw.tsx @@ -0,0 +1,25 @@ +import { SVGProps } from "react"; + +export const DrawIcon = ({ + fill = "currentColor", + ...props +}: JSX.IntrinsicAttributes & SVGProps) => { + return ( + + + + + + ); +}; diff --git a/examples/ui-demo/src/components/icons/external-link.tsx b/examples/ui-demo/src/components/icons/external-link.tsx index edfc1bf71..682e3e031 100644 --- a/examples/ui-demo/src/components/icons/external-link.tsx +++ b/examples/ui-demo/src/components/icons/external-link.tsx @@ -1,7 +1,7 @@ import { SVGProps } from "react"; export const ExternalLinkIcon = ({ - stroke = "#475569", + stroke = "currentColor", ...props }: JSX.IntrinsicAttributes & SVGProps) => ( ) => { + return ( + + + + + + ); +}; diff --git a/examples/ui-demo/src/components/icons/loading.tsx b/examples/ui-demo/src/components/icons/loading.tsx new file mode 100644 index 000000000..5ba85cb4c --- /dev/null +++ b/examples/ui-demo/src/components/icons/loading.tsx @@ -0,0 +1,81 @@ +import { useConfig } from "@/app/state"; +export const LoadingIcon = () => { + const { + config: { + ui: { theme }, + }, + } = useConfig(); + const animationClass = + theme === "dark" ? "animate-ui-loading-dark" : "animate-ui-loading-light"; + + return ( + + + + + + + + + + + ); +}; diff --git a/examples/ui-demo/src/components/icons/receipt.tsx b/examples/ui-demo/src/components/icons/receipt.tsx new file mode 100644 index 000000000..1f17c2966 --- /dev/null +++ b/examples/ui-demo/src/components/icons/receipt.tsx @@ -0,0 +1,25 @@ +import { SVGProps } from "react"; + +export const ReceiptIcon = ({ + fill = "currentColor", + ...props +}: JSX.IntrinsicAttributes & SVGProps) => { + return ( + + + + + + ); +}; diff --git a/examples/ui-demo/src/components/preview/AuthCardWrapper.tsx b/examples/ui-demo/src/components/preview/AuthCardWrapper.tsx index 830065f47..2003c514a 100644 --- a/examples/ui-demo/src/components/preview/AuthCardWrapper.tsx +++ b/examples/ui-demo/src/components/preview/AuthCardWrapper.tsx @@ -1,40 +1,27 @@ import { useConfig } from "@/app/state"; import { cn } from "@/lib/utils"; -import { AuthCard, useLogout, useUser } from "@account-kit/react"; +import { AuthCard, useUser } from "@account-kit/react"; +import { MintCard } from "../shared/MintCard"; export function AuthCardWrapper({ className }: { className?: string }) { const user = useUser(); const { config } = useConfig(); - const { logout } = useLogout(); return (
- { - !user ? ( - <> -
-
- -
-
- - ) : null - // In flight- will be uncommented in the mint pr, a fast follow - } - {user && ( - + {!user ? ( +
+
+ +
+
+ ) : ( + )}
); diff --git a/examples/ui-demo/src/components/preview/MintDemoWrapper.tsx b/examples/ui-demo/src/components/preview/MintDemoWrapper.tsx deleted file mode 100644 index 0f46f0512..000000000 --- a/examples/ui-demo/src/components/preview/MintDemoWrapper.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { UserConnectionAvatarWithPopover } from "@/components/shared/user-connection-avatar/UserConnectionAvatarWithPopover"; - -export function MintDemoWrapper() { - return ( -
-
- -
-
- { - // Rob - } -
-
- ); -} diff --git a/examples/ui-demo/src/components/shared/MintCard.tsx b/examples/ui-demo/src/components/shared/MintCard.tsx new file mode 100644 index 000000000..67c6daba9 --- /dev/null +++ b/examples/ui-demo/src/components/shared/MintCard.tsx @@ -0,0 +1,265 @@ +"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 { + 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"; + +type NFTLoadingState = "loading" | "success"; + +const initialState = { + signing: "signing", + gas: "gas", + batch: "batch", +} satisfies MintStatus; + +type MintStatus = { + signing: NFTLoadingState | "signing"; + gas: NFTLoadingState | "gas"; + batch: NFTLoadingState | "batch"; +}; + +export const MintCard = () => { + const [status, setStatus] = useState(initialState); + // To be wired into the toast pr + const [hasError, setHasError] = useState(false); + const { nftTransfered, setNFTTransfered } = useConfig(); + + const handleSuccess = () => { + setStatus(() => ({ + batch: "success", + gas: "success", + signing: "success", + })); + setNFTTransfered(true); + }; + const handleError = () => { + setStatus(initialState); + setHasError(true); + }; + + const { client } = useSmartAccountClient({ type: "LightAccount" }); + const { sendUserOperationResult, sendUserOperation } = useSendUserOperation({ + client, + waitForTxn: true, + onError: handleError, + onSuccess: handleSuccess, + onMutate: () => { + setTimeout(() => { + setStatus((prev) => ({ ...prev, signing: "success" })); + }, 500); + setTimeout(() => { + setStatus((prev) => ({ ...prev, gas: "success" })); + }, 750); + }, + }); + + const handleCollectNFT = useCallback(async () => { + if (!client) { + console.error("no client"); + return; + } + setStatus({ + signing: "loading", + gas: "loading", + batch: "loading", + }); + sendUserOperation({ + uo: { + target: nftContractAddress, + data: encodeFunctionData({ + abi: AccountKitNftMinterABI, + functionName: "mintTo", + args: [client.getAddress()], + }), + }, + }); + }, [client, sendUserOperation]); + const { data: uri } = useQuery({ + queryKey: ["contractURI", nftContractAddress], + queryFn: async () => { + const uri = await client?.readContract({ + address: nftContractAddress, + abi: AccountKitNftMinterABI, + functionName: "baseURI", + }); + console.log("uri", uri); + return uri; + + }, + enabled: !!client && !!client?.readContract, + }); + + return ( +
+
+

+ {!nftTransfered ? "One-click checkout" : "You collected your NFT!"} +

+ + + + Sponsor gas fees to remove barriers to adoption.{" "} + + Learn how. + + + } + /> + +
+
+

+ NFT Summary +

+ {uri ? ( +
+ {nftTransfered && ( +
+ Collected +
+ )} + An NFT +
+ ) : ( +
+ +
+ )} +
+

Gas Fee

+

+ + $0.02 + + + Free + +

+
+ {!nftTransfered ? ( + + ) : ( + + )} + + View docs + +
+
+ ); +}; + +const ValueProp = ({ + icon, + title, + description, +}: { + icon: "signing" | "gas" | "batch" | "loading" | "success"; + title: string; + description: string | JSX.Element; +}) => { + return ( +
+ {getMintIcon(icon)} +
+

{title}

+

{description}

+
+
+ ); +}; + +const getMintIcon = ( + icon: "signing" | "gas" | "batch" | "loading" | "success" +) => { + switch (icon) { + case "signing": + return ; + case "gas": + return ; + case "batch": + return ; + case "loading": + return ; + case "success": + return ; + } +}; diff --git a/examples/ui-demo/src/utils/config.ts b/examples/ui-demo/src/utils/config.ts new file mode 100644 index 000000000..b77a46237 --- /dev/null +++ b/examples/ui-demo/src/utils/config.ts @@ -0,0 +1,237 @@ +export const nftContractAddress = "0x92ccF22A61f92d83463b04090A32dA9a6D958f64"; +export const AccountKitNftMinterABI = [ + { + type: "constructor", + inputs: [ + { name: "_name", type: "string", internalType: "string" }, + { name: "_symbol", type: "string", internalType: "string" }, + { name: "_baseURI", type: "string", internalType: "string" }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "approve", + inputs: [ + { name: "spender", type: "address", internalType: "address" }, + { name: "id", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "balanceOf", + inputs: [{ name: "owner", type: "address", internalType: "address" }], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "baseURI", + inputs: [], + outputs: [{ name: "", type: "string", internalType: "string" }], + stateMutability: "view", + }, + { + type: "function", + name: "currentTokenId", + inputs: [], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "getApproved", + inputs: [{ name: "", type: "uint256", internalType: "uint256" }], + outputs: [{ name: "", type: "address", internalType: "address" }], + stateMutability: "view", + }, + { + type: "function", + name: "isApprovedForAll", + inputs: [ + { name: "", type: "address", internalType: "address" }, + { name: "", type: "address", internalType: "address" }, + ], + outputs: [{ name: "", type: "bool", internalType: "bool" }], + stateMutability: "view", + }, + { + type: "function", + name: "mintTo", + inputs: [{ name: "recipient", type: "address", internalType: "address" }], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "payable", + }, + { + type: "function", + name: "name", + inputs: [], + outputs: [{ name: "", type: "string", internalType: "string" }], + stateMutability: "view", + }, + { + type: "function", + name: "owner", + inputs: [], + outputs: [{ name: "", type: "address", internalType: "address" }], + stateMutability: "view", + }, + { + type: "function", + name: "ownerOf", + inputs: [{ name: "id", type: "uint256", internalType: "uint256" }], + outputs: [{ name: "owner", type: "address", internalType: "address" }], + stateMutability: "view", + }, + { + type: "function", + name: "renounceOwnership", + inputs: [], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "safeTransferFrom", + inputs: [ + { name: "from", type: "address", internalType: "address" }, + { name: "to", type: "address", internalType: "address" }, + { name: "id", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "safeTransferFrom", + inputs: [ + { name: "from", type: "address", internalType: "address" }, + { name: "to", type: "address", internalType: "address" }, + { name: "id", type: "uint256", internalType: "uint256" }, + { name: "data", type: "bytes", internalType: "bytes" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "setApprovalForAll", + inputs: [ + { name: "operator", type: "address", internalType: "address" }, + { name: "approved", type: "bool", internalType: "bool" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "supportsInterface", + inputs: [{ name: "interfaceId", type: "bytes4", internalType: "bytes4" }], + outputs: [{ name: "", type: "bool", internalType: "bool" }], + stateMutability: "view", + }, + { + type: "function", + name: "symbol", + inputs: [], + outputs: [{ name: "", type: "string", internalType: "string" }], + stateMutability: "view", + }, + { + type: "function", + name: "tokenURI", + inputs: [{ name: "tokenId", type: "uint256", internalType: "uint256" }], + outputs: [{ name: "", type: "string", internalType: "string" }], + stateMutability: "view", + }, + { + type: "function", + name: "transferFrom", + inputs: [ + { name: "from", type: "address", internalType: "address" }, + { name: "to", type: "address", internalType: "address" }, + { name: "id", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "transferOwnership", + inputs: [{ name: "newOwner", type: "address", internalType: "address" }], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "event", + name: "Approval", + inputs: [ + { + name: "owner", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "spender", + type: "address", + indexed: true, + internalType: "address", + }, + { name: "id", type: "uint256", indexed: true, internalType: "uint256" }, + ], + anonymous: false, + }, + { + type: "event", + name: "ApprovalForAll", + inputs: [ + { + name: "owner", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "operator", + type: "address", + indexed: true, + internalType: "address", + }, + { name: "approved", type: "bool", indexed: false, internalType: "bool" }, + ], + anonymous: false, + }, + { + type: "event", + name: "OwnershipTransferred", + inputs: [ + { + name: "previousOwner", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "newOwner", + type: "address", + indexed: true, + internalType: "address", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "Transfer", + inputs: [ + { name: "from", type: "address", indexed: true, internalType: "address" }, + { name: "to", type: "address", indexed: true, internalType: "address" }, + { name: "id", type: "uint256", indexed: true, internalType: "uint256" }, + ], + anonymous: false, + }, + { type: "error", name: "NonExistentTokenURI", inputs: [] }, +] as const; diff --git a/examples/ui-demo/tailwind.config.ts b/examples/ui-demo/tailwind.config.ts index 0272e9583..2df190801 100644 --- a/examples/ui-demo/tailwind.config.ts +++ b/examples/ui-demo/tailwind.config.ts @@ -3,11 +3,11 @@ import type { Config } from "tailwindcss"; const config = { content: [ - './pages/**/*.{ts,tsx}', - './components/**/*.{ts,tsx}', - './app/**/*.{ts,tsx}', - './src/**/*.{ts,tsx}', - ], + "./pages/**/*.{ts,tsx}", + "./components/**/*.{ts,tsx}", + "./app/**/*.{ts,tsx}", + "./src/**/*.{ts,tsx}", + ], prefix: "", theme: { container: { @@ -68,14 +68,24 @@ const config = { from: { height: "var(--radix-accordion-content-height)" }, to: { height: "0" }, }, + "ui-loading-light": { + "0%, 20%, to": { fill: "#cbd5e1" }, + "10%": { fill: "#363ff9" }, + }, + "ui-loading-dark": { + "0%, 20%, to": { fill: "rgba(255, 255, 255, .5)" }, + "10%": { fill: "#FFFFFF" }, + }, }, animation: { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", + "ui-loading-dark": "ui-loading-dark 0.8s 0ms ease-out infinite both", + "ui-loading-light": "ui-loading-light 0.8s 0ms ease-out infinite both", }, }, }, - plugins: [require("tailwindcss-animate"), require('tailwind-scrollbar')], -} satisfies Config + plugins: [require("tailwindcss-animate"), require("tailwind-scrollbar")], +} satisfies Config; export default withAccountKitUi(config);