diff --git a/packages/create-onchain/templates/minikit-basic/README.md b/packages/create-onchain/templates/minikit-basic/README.md new file mode 100644 index 0000000000..29a0a18fb8 --- /dev/null +++ b/packages/create-onchain/templates/minikit-basic/README.md @@ -0,0 +1,112 @@ +# MiniKit Template + +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-onchain --mini`](), configured with: + +- [MiniKit](https://docs.base.org/builderkits/minikit/overview) +- [OnchainKit](https://www.base.org/builders/onchainkit) +- [Tailwind CSS](https://tailwindcss.com) +- [Next.js](https://nextjs.org/docs) + +## Getting Started + +1. Install dependencies: +```bash +npm install +# or +yarn install +# or +pnpm install +# or +bun install +``` + +2. Verify environment variables, these will be set up by the `npx create-onchain --mini` command: + +You can regenerate the FARCASTER Account Association environment variables by running `npx create-onchain --manifest` in your project directory. + +The environment variables enable the following features: + +- Frame metadata - Sets up the Frame Embed that will be shown when you cast your frame +- Account association - Allows users to add your frame to their account, enables notifications +- Redis API keys - Enable Webhooks and background notifications for your application by storing users notification details + +```bash +# Shared/OnchainKit variables +NEXT_PUBLIC_ONCHAINKIT_PROJECT_NAME= +NEXT_PUBLIC_URL= +NEXT_PUBLIC_ICON_URL= +NEXT_PUBLIC_ONCHAINKIT_API_KEY= + +# Frame metadata +FARCASTER_HEADER= +FARCASTER_PAYLOAD= +FARCASTER_SIGNATURE= +NEXT_PUBLIC_APP_ICON= +NEXT_PUBLIC_APP_SUBTITLE= +NEXT_PUBLIC_APP_DESCRIPTION= +NEXT_PUBLIC_APP_SPLASH_IMAGE= +NEXT_PUBLIC_SPLASH_BACKGROUND_COLOR= +NEXT_PUBLIC_APP_PRIMARY_CATEGORY= +NEXT_PUBLIC_APP_HERO_IMAGE= +NEXT_PUBLIC_APP_TAGLINE= +NEXT_PUBLIC_APP_OG_TITLE= +NEXT_PUBLIC_APP_OG_DESCRIPTION= +NEXT_PUBLIC_APP_OG_IMAGE= + +# Redis config +REDIS_URL= +REDIS_TOKEN= +``` + +3. Start the development server: +```bash +npm run dev +``` + +## Template Features + +### Frame Configuration +- `.well-known/farcaster.json` endpoint configured for Frame metadata and account association +- Frame metadata automatically added to page headers in `layout.tsx` + +### Background Notifications +- Redis-backed notification system using Upstash +- Ready-to-use notification endpoints in `api/notify` and `api/webhook` +- Notification client utilities in `lib/notification-client.ts` + +### Theming +- Custom theme defined in `theme.css` with OnchainKit variables +- Pixel font integration with Pixelify Sans +- Dark/light mode support through OnchainKit + +### MiniKit Provider +The app is wrapped with `MiniKitProvider` in `providers.tsx`, configured with: +- OnchainKit integration +- Access to Frames context +- Sets up Wagmi Connectors +- Sets up Frame SDK listeners +- Applies Safe Area Insets + +## Customization + +To get started building your own frame, follow these steps: + +1. Remove the DemoComponents: + - Delete `components/DemoComponents.tsx` + - Remove demo-related imports from `page.tsx` + +2. Start building your Frame: + - Modify `page.tsx` to create your Frame UI + - Update theme variables in `theme.css` + - Adjust MiniKit configuration in `providers.tsx` + +3. Add your frame to your account: + - Cast your frame to see it in action + - Share your frame with others to start building your community + +## Learn More + +- [MiniKit Documentation](https://docs.base.org/builderkits/minikit/overview) +- [OnchainKit Documentation](https://docs.base.org/builderkits/onchainkit/getting-started) +- [Next.js Documentation](https://nextjs.org/docs) +- [Tailwind CSS Documentation](https://tailwindcss.com/docs) diff --git a/packages/create-onchain/templates/minikit-basic/app/.well-known/farcaster.json/route.ts b/packages/create-onchain/templates/minikit-basic/app/.well-known/farcaster.json/route.ts new file mode 100644 index 0000000000..3c6a7f8cb0 --- /dev/null +++ b/packages/create-onchain/templates/minikit-basic/app/.well-known/farcaster.json/route.ts @@ -0,0 +1,43 @@ +function withValidProperties( + properties: Record, +) { + return Object.fromEntries( + Object.entries(properties).filter(([key, value]) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return !!value; + }), + ); +} + +export async function GET() { + const URL = process.env.NEXT_PUBLIC_URL; + + return Response.json({ + accountAssociation: { + header: process.env.FARCASTER_HEADER, + payload: process.env.FARCASTER_PAYLOAD, + signature: process.env.FARCASTER_SIGNATURE, + }, + frame: withValidProperties({ + version: "1", + name: process.env.NEXT_PUBLIC_ONCHAINKIT_PROJECT_NAME, + subtitle: process.env.NEXT_PUBLIC_APP_SUBTITLE, + description: process.env.NEXT_PUBLIC_APP_DESCRIPTION, + screenshotUrls: [], + iconUrl: process.env.NEXT_PUBLIC_APP_ICON, + splashImageUrl: process.env.NEXT_PUBLIC_APP_SPLASH_IMAGE, + splashBackgroundColor: process.env.NEXT_PUBLIC_SPLASH_BACKGROUND_COLOR, + homeUrl: URL, + webhookUrl: `${URL}/api/webhook`, + primaryCategory: process.env.NEXT_PUBLIC_APP_PRIMARY_CATEGORY, + tags: [], + heroImageUrl: process.env.NEXT_PUBLIC_APP_HERO_IMAGE, + tagline: process.env.NEXT_PUBLIC_APP_TAGLINE, + ogTitle: process.env.NEXT_PUBLIC_APP_OG_TITLE, + ogDescription: process.env.NEXT_PUBLIC_APP_OG_DESCRIPTION, + ogImageUrl: process.env.NEXT_PUBLIC_APP_OG_IMAGE, + }), + }); +} diff --git a/packages/create-onchain/templates/minikit-basic/public/hero.png b/packages/create-onchain/templates/minikit-basic/public/hero.png new file mode 100644 index 0000000000..c5ec486188 Binary files /dev/null and b/packages/create-onchain/templates/minikit-basic/public/hero.png differ diff --git a/packages/create-onchain/templates/minikit-basic/public/icon.png b/packages/create-onchain/templates/minikit-basic/public/icon.png new file mode 100644 index 0000000000..4e3c828cfc Binary files /dev/null and b/packages/create-onchain/templates/minikit-basic/public/icon.png differ diff --git a/packages/create-onchain/templates/minikit-basic/public/logo.png b/packages/create-onchain/templates/minikit-basic/public/logo.png new file mode 100644 index 0000000000..f2738288f2 Binary files /dev/null and b/packages/create-onchain/templates/minikit-basic/public/logo.png differ diff --git a/packages/create-onchain/templates/minikit-basic/public/screenshot.png b/packages/create-onchain/templates/minikit-basic/public/screenshot.png new file mode 100644 index 0000000000..46987b6ee2 Binary files /dev/null and b/packages/create-onchain/templates/minikit-basic/public/screenshot.png differ diff --git a/packages/create-onchain/templates/minikit-basic/public/splash.png b/packages/create-onchain/templates/minikit-basic/public/splash.png new file mode 100644 index 0000000000..1575951b71 Binary files /dev/null and b/packages/create-onchain/templates/minikit-basic/public/splash.png differ diff --git a/packages/create-onchain/templates/minikit-snake/app/.well-known/farcaster.json/route.ts b/packages/create-onchain/templates/minikit-snake/app/.well-known/farcaster.json/route.ts new file mode 100644 index 0000000000..3c6a7f8cb0 --- /dev/null +++ b/packages/create-onchain/templates/minikit-snake/app/.well-known/farcaster.json/route.ts @@ -0,0 +1,43 @@ +function withValidProperties( + properties: Record, +) { + return Object.fromEntries( + Object.entries(properties).filter(([key, value]) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return !!value; + }), + ); +} + +export async function GET() { + const URL = process.env.NEXT_PUBLIC_URL; + + return Response.json({ + accountAssociation: { + header: process.env.FARCASTER_HEADER, + payload: process.env.FARCASTER_PAYLOAD, + signature: process.env.FARCASTER_SIGNATURE, + }, + frame: withValidProperties({ + version: "1", + name: process.env.NEXT_PUBLIC_ONCHAINKIT_PROJECT_NAME, + subtitle: process.env.NEXT_PUBLIC_APP_SUBTITLE, + description: process.env.NEXT_PUBLIC_APP_DESCRIPTION, + screenshotUrls: [], + iconUrl: process.env.NEXT_PUBLIC_APP_ICON, + splashImageUrl: process.env.NEXT_PUBLIC_APP_SPLASH_IMAGE, + splashBackgroundColor: process.env.NEXT_PUBLIC_SPLASH_BACKGROUND_COLOR, + homeUrl: URL, + webhookUrl: `${URL}/api/webhook`, + primaryCategory: process.env.NEXT_PUBLIC_APP_PRIMARY_CATEGORY, + tags: [], + heroImageUrl: process.env.NEXT_PUBLIC_APP_HERO_IMAGE, + tagline: process.env.NEXT_PUBLIC_APP_TAGLINE, + ogTitle: process.env.NEXT_PUBLIC_APP_OG_TITLE, + ogDescription: process.env.NEXT_PUBLIC_APP_OG_DESCRIPTION, + ogImageUrl: process.env.NEXT_PUBLIC_APP_OG_IMAGE, + }), + }); +} diff --git a/packages/create-onchain/templates/minikit-snake/app/layout.tsx b/packages/create-onchain/templates/minikit-snake/app/layout.tsx new file mode 100644 index 0000000000..1c2f5de727 --- /dev/null +++ b/packages/create-onchain/templates/minikit-snake/app/layout.tsx @@ -0,0 +1,50 @@ +import "./theme.css"; +import "@coinbase/onchainkit/styles.css"; +import type { Metadata, Viewport } from "next"; +import "./globals.css"; +import { Providers } from "./providers"; + +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, +}; + +export async function generateMetadata(): Promise { + const URL = process.env.NEXT_PUBLIC_URL; + return { + title: process.env.NEXT_PUBLIC_ONCHAINKIT_PROJECT_NAME, + description: + "Generated by `create-onchain --template=minikit-snake`, a Next.js template for MiniKit", + other: { + "fc:frame": JSON.stringify({ + version: "next", + imageUrl: process.env.NEXT_PUBLIC_APP_HERO_IMAGE, + button: { + title: `Launch ${process.env.NEXT_PUBLIC_ONCHAINKIT_PROJECT_NAME}`, + action: { + type: "launch_frame", + name: process.env.NEXT_PUBLIC_ONCHAINKIT_PROJECT_NAME, + url: URL, + splashImageUrl: process.env.NEXT_PUBLIC_SPLASH_IMAGE, + splashBackgroundColor: + process.env.NEXT_PUBLIC_SPLASH_BACKGROUND_COLOR, + }, + }, + }), + }, + }; +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/packages/create-onchain/templates/minikit-snake/public/hero.png b/packages/create-onchain/templates/minikit-snake/public/hero.png new file mode 100644 index 0000000000..c5ec486188 Binary files /dev/null and b/packages/create-onchain/templates/minikit-snake/public/hero.png differ diff --git a/packages/create-onchain/templates/minikit-snake/public/icon.png b/packages/create-onchain/templates/minikit-snake/public/icon.png new file mode 100644 index 0000000000..4e3c828cfc Binary files /dev/null and b/packages/create-onchain/templates/minikit-snake/public/icon.png differ diff --git a/packages/create-onchain/templates/minikit-snake/public/screenshot.png b/packages/create-onchain/templates/minikit-snake/public/screenshot.png new file mode 100644 index 0000000000..46987b6ee2 Binary files /dev/null and b/packages/create-onchain/templates/minikit-snake/public/screenshot.png differ diff --git a/packages/create-onchain/templates/minikit-snake/public/splash.png b/packages/create-onchain/templates/minikit-snake/public/splash.png new file mode 100644 index 0000000000..1575951b71 Binary files /dev/null and b/packages/create-onchain/templates/minikit-snake/public/splash.png differ diff --git a/packages/onchainkit/src/minikit/hooks/useMiniKit.test.tsx b/packages/onchainkit/src/minikit/hooks/useMiniKit.test.tsx index d41d653c2f..4e2692c2a5 100644 --- a/packages/onchainkit/src/minikit/hooks/useMiniKit.test.tsx +++ b/packages/onchainkit/src/minikit/hooks/useMiniKit.test.tsx @@ -67,6 +67,22 @@ describe('useMiniKit', () => { expect(result.current.isFrameReady).toBe(true); }); + it('allows users to pass through ready options', async () => { + const { result } = renderHook(() => useMiniKit(), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + await act(async () => { + result.current.setFrameReady({ disableNativeGestures: true }); + }); + + expect(result.current.isFrameReady).toBe(true); + }); + it('should return the correct values', () => { const { result } = renderHook(() => useMiniKit(), { wrapper: ({ children }) => ( diff --git a/packages/onchainkit/src/transaction/hooks/useWriteContracts.test.ts b/packages/onchainkit/src/transaction/hooks/useWriteContracts.test.ts new file mode 100644 index 0000000000..91ef969250 --- /dev/null +++ b/packages/onchainkit/src/transaction/hooks/useWriteContracts.test.ts @@ -0,0 +1,155 @@ +import { renderHook } from '@testing-library/react'; +import type { TransactionExecutionError } from 'viem'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useWriteContracts as useWriteContractsWagmi } from 'wagmi/experimental'; +import { useWriteContracts } from './useWriteContracts'; + +vi.mock('wagmi/experimental', () => ({ + useWriteContracts: vi.fn(), +})); + +interface UseWriteContractsConfig { + mutation: { + /* eslint-disable @typescript-eslint/no-explicit-any */ + onError?: (error: any) => void; + onSuccess?: (data: { id: string }) => void; + }; +} + +describe('useWriteContracts', () => { + const mockSetLifecycleStatus = vi.fn(); + const mockSetTransactionId = vi.fn(); + + beforeEach(() => { + vi.resetAllMocks(); + }); + + it('should handle generic error', () => { + const genericError = new Error('Something went wrong. Please try again.'); + let onErrorCallback: ((error: Error) => void) | undefined; + (useWriteContractsWagmi as ReturnType).mockImplementation( + ({ mutation }: UseWriteContractsConfig) => { + onErrorCallback = mutation.onError; + return { + writeContracts: vi.fn(), + status: 'error', + }; + }, + ); + renderHook(() => + useWriteContracts({ + setLifecycleStatus: mockSetLifecycleStatus, + setTransactionId: mockSetTransactionId, + }), + ); + expect(onErrorCallback).toBeDefined(); + onErrorCallback?.(genericError); + expect(mockSetLifecycleStatus).toHaveBeenCalledWith({ + statusName: 'error', + statusData: { + code: 'TmUWCSh01', + error: 'Something went wrong. Please try again.', + message: 'Something went wrong. Please try again.', + }, + }); + }); + + it('should handle userRejectedRequestError', () => { + let onErrorCallback: + | ((error: TransactionExecutionError) => void) + | undefined; + (useWriteContractsWagmi as ReturnType).mockImplementation( + ({ mutation }: UseWriteContractsConfig) => { + onErrorCallback = mutation.onError; + return { + writeContracts: vi.fn(), + status: 'error', + }; + }, + ); + renderHook(() => + useWriteContracts({ + setLifecycleStatus: mockSetLifecycleStatus, + setTransactionId: mockSetTransactionId, + }), + ); + expect(onErrorCallback).toBeDefined(); + onErrorCallback?.({ + cause: { + name: 'UserRejectedRequestError', + details: 'User rejected the request', + shortMessage: 'User rejected', + message: 'User rejected the transaction', + version: '1.0', + walk: () => {}, + }, + message: 'Request denied.', + } as TransactionExecutionError); + expect(mockSetLifecycleStatus).toHaveBeenCalledWith({ + statusName: 'error', + statusData: { + code: 'TmUWCSh01', + error: 'Request denied.', + message: 'Request denied.', + }, + }); + }); + + it('should handle EOA specific error to fallback to writeContract', () => { + let onErrorCallback: + | ((error: TransactionExecutionError) => void) + | undefined; + (useWriteContractsWagmi as ReturnType).mockImplementation( + ({ mutation }: UseWriteContractsConfig) => { + onErrorCallback = mutation.onError; + return { + writeContracts: vi.fn(), + status: 'error', + message: 'this request method is not supported', + }; + }, + ); + renderHook(() => + useWriteContracts({ + setLifecycleStatus: mockSetLifecycleStatus, + setTransactionId: mockSetTransactionId, + }), + ); + expect(onErrorCallback).toBeDefined(); + onErrorCallback?.({ + cause: { + name: 'eoa-error', + details: 'EOA error details', + shortMessage: 'EOA error', + message: 'Error with EOA', + version: '1.0', + walk: () => {}, + }, + message: 'this request method is not supported', + } as TransactionExecutionError); + expect(mockSetLifecycleStatus).not.toHaveBeenCalled(); + }); + + it('should handle successful transaction', () => { + const transactionId = { id: '0x123' }; + let onSuccessCallback: ((data: { id: string }) => void) | undefined; + (useWriteContractsWagmi as ReturnType).mockImplementation( + ({ mutation }: UseWriteContractsConfig) => { + onSuccessCallback = mutation.onSuccess; + return { + writeContracts: vi.fn(), + status: 'success', + }; + }, + ); + renderHook(() => + useWriteContracts({ + setLifecycleStatus: mockSetLifecycleStatus, + setTransactionId: mockSetTransactionId, + }), + ); + expect(onSuccessCallback).toBeDefined(); + onSuccessCallback?.(transactionId); + expect(mockSetTransactionId).toHaveBeenCalledWith(transactionId.id); + }); +}); diff --git a/packages/onchainkit/src/transaction/hooks/useWriteContracts.ts b/packages/onchainkit/src/transaction/hooks/useWriteContracts.ts new file mode 100644 index 0000000000..e3d31a70df --- /dev/null +++ b/packages/onchainkit/src/transaction/hooks/useWriteContracts.ts @@ -0,0 +1,45 @@ +import { useWriteContracts as useWriteContractsWagmi } from 'wagmi/experimental'; +import { + GENERIC_ERROR_MESSAGE, + METHOD_NOT_SUPPORTED_ERROR_SUBSTRING, +} from '../constants'; +import type { UseWriteContractsParams } from '../types'; +import { isUserRejectedRequestError } from '../utils/isUserRejectedRequestError'; +import { normalizeTransactionId } from '@/internal/utils/normalizeWagmi'; + +/** + * useWriteContracts: Experimental Wagmi hook for batching transactions. + * Supports Smart Wallets. + * Supports batch operations and capabilities such as paymasters. + * Does not support EOAs. + */ +export function useWriteContracts({ + setLifecycleStatus, + setTransactionId, +}: UseWriteContractsParams) { + const { status, writeContractsAsync } = useWriteContractsWagmi({ + mutation: { + onError: (e) => { + // Ignore EOA-specific error to fallback to writeContract + if (e.message.includes(METHOD_NOT_SUPPORTED_ERROR_SUBSTRING)) { + return; + } + const errorMessage = isUserRejectedRequestError(e) + ? 'Request denied.' + : GENERIC_ERROR_MESSAGE; + setLifecycleStatus({ + statusName: 'error', + statusData: { + code: 'TmUWCSh01', // Transaction module UseWriteContracts hook 01 error + error: e.message, + message: errorMessage, + }, + }); + }, + onSuccess: (data) => { + setTransactionId(normalizeTransactionId(data)); + }, + }, + }); + return { status, writeContractsAsync }; +} diff --git a/packages/onchainkit/src/utils/crypto.test.ts b/packages/onchainkit/src/utils/crypto.test.ts new file mode 100644 index 0000000000..66c44ef27b --- /dev/null +++ b/packages/onchainkit/src/utils/crypto.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it, vi } from 'vitest'; +import { generateUUIDWithInsecureFallback } from './crypto'; +import { beforeEach } from 'node:test'; + +describe('generateUUIDWithInsecureFallback', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should generate a UUID when crypto is defined', () => { + vi.mock('crypto', () => ({ + randomUUID: vi.fn(() => '123e4567-e89b-12d3-a456-426614174000'), + })); + + const uuid = generateUUIDWithInsecureFallback(); + expect(uuid).toBeDefined(); + expect(uuid.length).toBe(36); + }); + + it('should generate a UUID when crypto is not defined', () => { + vi.stubGlobal('crypto', { + randomUUID: undefined, + getRandomValues: () => new Uint8Array(16), + }); + + const uuid = generateUUIDWithInsecureFallback(); + expect(uuid).toBeDefined(); + expect(uuid.length).toBe(36); + }); +}); diff --git a/packages/onchainkit/src/utils/crypto.ts b/packages/onchainkit/src/utils/crypto.ts new file mode 100644 index 0000000000..e696b4bbfd --- /dev/null +++ b/packages/onchainkit/src/utils/crypto.ts @@ -0,0 +1,46 @@ +// Secure fallback using crypto.getRandomValues +const getRandomValues = (size: number) => { + const array = new Uint8Array(size); + crypto.getRandomValues(array); + return array; +}; + +/** + * Generates a UUID using the crypto API if available, otherwise falls back to a + * more insecure method. Only use this for non-critical purposes. + * + * @returns A UUID string that may not be cryptographically secure + */ +export const generateUUIDWithInsecureFallback = () => { + if ( + typeof crypto !== 'undefined' && + typeof crypto.randomUUID === 'function' + ) { + return crypto.randomUUID(); + } + + const bytes = getRandomValues(16); + // Format the random bytes into a UUID string + return ( + bytes[0].toString(16).padStart(2, '0') + + bytes[1].toString(16).padStart(2, '0') + + bytes[2].toString(16).padStart(2, '0') + + bytes[3].toString(16).padStart(2, '0') + + '-' + + bytes[4].toString(16).padStart(2, '0') + + bytes[5].toString(16).padStart(2, '0') + + '-' + + ((bytes[6] & 0x0f) | 0x40).toString(16).padStart(2, '0') + // UUID version 4 + bytes[7].toString(16).padStart(2, '0') + + '-' + + ((bytes[8] & 0x3f) | 0x80).toString(16).padStart(2, '0') + // UUID variant + bytes[9].toString(16).padStart(2, '0') + + '-' + + bytes[10].toString(16).padStart(2, '0') + + bytes[11].toString(16).padStart(2, '0') + + bytes[12].toString(16).padStart(2, '0') + + bytes[13].toString(16).padStart(2, '0') + + bytes[14].toString(16).padStart(2, '0') + + bytes[15].toString(16).padStart(2, '0') + ); +}; diff --git a/packages/playground/components/AppProvider.tsx b/packages/playground/components/AppProvider.tsx index cfefe72eb1..22052a915c 100644 --- a/packages/playground/components/AppProvider.tsx +++ b/packages/playground/components/AppProvider.tsx @@ -42,6 +42,8 @@ type State = { isSponsored?: boolean; vaultAddress?: Address; setVaultAddress: (vaultAddress: Address) => void; + isSignUpEnabled: boolean; + setIsSignUpEnabled: (isSignUpEnabled: boolean) => void; }; export const defaultState: State = { @@ -54,6 +56,8 @@ export const defaultState: State = { setNFTToken: () => {}, setIsSponsored: () => {}, setVaultAddress: () => {}, + isSignUpEnabled: true, + setIsSignUpEnabled: () => {}, }; export const AppContext = createContext(defaultState); @@ -135,6 +139,12 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { defaultValue: '0x7BfA7C4f149E7415b73bdeDfe609237e29CBF34A', }); + const [isSignUpEnabled, setIsSignUpEnabled] = useStateWithStorage({ + key: 'isSignUpEnabled', + defaultValue: true, + parser: (v) => v === 'true', + }); + // Load initial values from localStorage useEffect(() => { const storedPaymasters = localStorage.getItem('paymasters'); @@ -180,6 +190,8 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { isSponsored, vaultAddress, setVaultAddress, + isSignUpEnabled, + setIsSignUpEnabled, }} > { paymaster: paymasters?.[chainId || 8453]?.url, wallet: { display: 'modal', + signUpEnabled: isSignUpEnabled, termsUrl: 'https://www.coinbase.com/legal/cookie', privacyUrl: 'https://www.coinbase.com/legal/privacy', supportedWallets: { diff --git a/packages/playground/components/form/wallet-sign-up.tsx b/packages/playground/components/form/wallet-sign-up.tsx new file mode 100644 index 0000000000..085bad345b --- /dev/null +++ b/packages/playground/components/form/wallet-sign-up.tsx @@ -0,0 +1,37 @@ +import { Label } from '@/components/ui/label'; +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; +import { useContext } from 'react'; +import { AppContext } from '@/components/AppProvider'; + +export function WalletSignUp() { + const { isSignUpEnabled, setIsSignUpEnabled } = useContext(AppContext); + + return ( +
+ + setIsSignUpEnabled(value === 'enabled')} + > +
+ + +
+
+
+ ); +}