diff --git a/src/OnchainKitProvider.test.tsx b/src/OnchainKitProvider.test.tsx
index 812c7ac227..520792a4a0 100644
--- a/src/OnchainKitProvider.test.tsx
+++ b/src/OnchainKitProvider.test.tsx
@@ -2,14 +2,38 @@ import '@testing-library/jest-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render, screen, waitFor } from '@testing-library/react';
import { base } from 'viem/chains';
-import { describe, expect, it, vi } from 'vitest';
+import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest';
import { http, WagmiProvider, createConfig } from 'wagmi';
+import { useConfig } from 'wagmi';
import { mock } from 'wagmi/connectors';
import { setOnchainKitConfig } from './OnchainKitConfig';
import { OnchainKitProvider } from './OnchainKitProvider';
import { COINBASE_VERIFIED_ACCOUNT_SCHEMA_ID } from './identity/constants';
import type { EASSchemaUid } from './identity/types';
import { useOnchainKit } from './useOnchainKit';
+import { useProviderDependencies } from './useProviderDependencies';
+
+vi.mock('wagmi', async (importOriginal) => {
+ const actual = await importOriginal();
+ return {
+ ...actual,
+ useConfig: vi.fn(),
+ };
+});
+
+vi.mock('./useProviderDependencies', () => ({
+ useProviderDependencies: vi.fn(() => ({
+ providedWagmiConfig: null,
+ providedQueryClient: null,
+ })),
+}));
+
+vi.mock('./useProviderDependencies', () => ({
+ useProviderDependencies: vi.fn(() => ({
+ providedWagmiConfig: null,
+ providedQueryClient: null,
+ })),
+}));
const queryClient = new QueryClient();
const mockConfig = createConfig({
@@ -51,8 +75,17 @@ describe('OnchainKitProvider', () => {
const apiKey = 'test-api-key';
const paymasterUrl =
'https://api.developer.coinbase.com/rpc/v1/base/test-api-key';
- const appLogo = undefined;
- const appName = undefined;
+ const appLogo = '';
+ const appName = 'Dapp';
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ (useConfig as Mock).mockReturnValue(mockConfig);
+ (useProviderDependencies as Mock).mockReturnValue({
+ providedWagmiConfig: mockConfig,
+ providedQueryClient: queryClient,
+ });
+ });
it('provides the context value correctly', async () => {
render(
@@ -71,6 +104,22 @@ describe('OnchainKitProvider', () => {
});
});
+ it('provides the context value correctly without WagmiProvider', async () => {
+ (useProviderDependencies as Mock).mockReturnValue({
+ providedWagmiConfig: null,
+ providedQueryClient: null,
+ });
+ render(
+
+
+ ,
+ );
+ await waitFor(() => {
+ expect(screen.getByText(schemaId)).toBeInTheDocument();
+ expect(screen.getByText(apiKey)).toBeInTheDocument();
+ });
+ });
+
it('throws an error if schemaId does not meet the required length', () => {
expect(() => {
render(
diff --git a/src/OnchainKitProvider.tsx b/src/OnchainKitProvider.tsx
index 401bbc28e0..5131e55590 100644
--- a/src/OnchainKitProvider.tsx
+++ b/src/OnchainKitProvider.tsx
@@ -1,8 +1,12 @@
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { createContext, useMemo } from 'react';
+import { WagmiProvider } from 'wagmi';
import { ONCHAIN_KIT_CONFIG, setOnchainKitConfig } from './OnchainKitConfig';
+import { createWagmiConfig } from './createWagmiConfig';
import { COINBASE_VERIFIED_ACCOUNT_SCHEMA_ID } from './identity/constants';
import { checkHashLength } from './internal/utils/checkHashLength';
import type { OnchainKitContextType, OnchainKitProviderReact } from './types';
+import { useProviderDependencies } from './useProviderDependencies';
export const OnchainKitContext =
createContext(ONCHAIN_KIT_CONFIG);
@@ -24,6 +28,7 @@ export function OnchainKitProvider({
throw Error('EAS schemaId must be 64 characters prefixed with "0x"');
}
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: ignore
const value = useMemo(() => {
const defaultPaymasterUrl = apiKey
? `https://api.developer.coinbase.com/rpc/v1/${chain.name
@@ -36,8 +41,8 @@ export function OnchainKitProvider({
chain: chain,
config: {
appearance: {
- name: config?.appearance?.name,
- logo: config?.appearance?.logo,
+ name: config?.appearance?.name ?? 'Dapp',
+ logo: config?.appearance?.logo ?? '',
mode: config?.appearance?.mode ?? 'auto',
theme: config?.appearance?.theme ?? 'default',
},
@@ -51,6 +56,47 @@ export function OnchainKitProvider({
return onchainKitConfig;
}, [address, apiKey, chain, config, projectId, rpcUrl, schemaId]);
+ // Check the React context for WagmiProvider and QueryClientProvider
+ const { providedWagmiConfig, providedQueryClient } =
+ useProviderDependencies();
+
+ const defaultConfig = useMemo(() => {
+ // IMPORTANT: Don't create a new Wagmi configuration if one already exists
+ // This prevents the user-provided WagmiConfig from being overriden
+ return (
+ providedWagmiConfig ||
+ createWagmiConfig({
+ apiKey,
+ appName: value.config.appearance.name,
+ appLogoUrl: value.config.appearance.logo,
+ })
+ );
+ }, [
+ apiKey,
+ providedWagmiConfig,
+ value.config.appearance.name,
+ value.config.appearance.logo,
+ ]);
+ const defaultQueryClient = useMemo(() => {
+ // IMPORTANT: Don't create a new QueryClient if one already exists
+ // This prevents the user-provided QueryClient from being overriden
+ return providedQueryClient || new QueryClient();
+ }, [providedQueryClient]);
+
+ // If both dependencies are missing, return a context with default parent providers
+ // If only one dependency is provided, expect the user to also provide the missing one
+ if (!providedWagmiConfig && !providedQueryClient) {
+ return (
+
+
+
+ {children}
+
+
+
+ );
+ }
+
return (
{children}
diff --git a/src/createWagmiConfig.test.ts b/src/createWagmiConfig.test.ts
new file mode 100644
index 0000000000..7fcda2f6ef
--- /dev/null
+++ b/src/createWagmiConfig.test.ts
@@ -0,0 +1,96 @@
+import { describe, expect, it, vi } from 'vitest';
+import { createConfig } from 'wagmi';
+import { http } from 'wagmi';
+import { base, baseSepolia } from 'wagmi/chains';
+import { coinbaseWallet } from 'wagmi/connectors';
+import { createWagmiConfig } from './createWagmiConfig';
+
+// Mock the imported modules
+vi.mock('wagmi', async () => {
+ const actual = await vi.importActual('wagmi');
+ return {
+ ...actual,
+ createConfig: vi.fn(),
+ createStorage: vi.fn(),
+ };
+});
+
+vi.mock('wagmi/chains', async () => {
+ const actual = await vi.importActual('wagmi/chains');
+ return {
+ ...actual,
+ base: { id: 8453 },
+ baseSepolia: { id: 84532 },
+ };
+});
+
+vi.mock('wagmi/connectors', async () => {
+ const actual = await vi.importActual('wagmi/connectors');
+ return {
+ ...actual,
+ coinbaseWallet: vi.fn(),
+ };
+});
+
+describe('createWagmiConfig', () => {
+ it('should create config with default values when no parameters are provided', () => {
+ createWagmiConfig({});
+ expect(createConfig).toHaveBeenCalledWith(
+ expect.objectContaining({
+ chains: [base, baseSepolia],
+ ssr: true,
+ transports: {
+ [base.id]: expect.any(Function),
+ [baseSepolia.id]: expect.any(Function),
+ },
+ }),
+ );
+ expect(coinbaseWallet).toHaveBeenCalledWith({
+ appName: undefined,
+ appLogoUrl: undefined,
+ preference: 'all',
+ });
+ });
+
+ it('should create config with custom values when parameters are provided', () => {
+ const customConfig = {
+ appearance: {
+ name: 'Custom App',
+ logo: 'https://example.com/logo.png',
+ },
+ };
+ createWagmiConfig({
+ apiKey: 'test-api-key',
+ appName: customConfig.appearance.name,
+ appLogoUrl: customConfig.appearance.logo,
+ });
+ expect(createConfig).toHaveBeenCalledWith(
+ expect.objectContaining({
+ chains: [base, baseSepolia],
+ ssr: true,
+ transports: {
+ [base.id]: expect.any(Function),
+ [baseSepolia.id]: expect.any(Function),
+ },
+ }),
+ );
+ expect(coinbaseWallet).toHaveBeenCalledWith({
+ appName: 'Custom App',
+ appLogoUrl: 'https://example.com/logo.png',
+ preference: 'all',
+ });
+ });
+
+ it('should use API key in transports when provided', () => {
+ const testApiKey = 'test-api-key';
+ const result = createWagmiConfig({ apiKey: testApiKey });
+ expect(result).toContain(
+ http(`https://api.developer.coinbase.com/rpc/v1/base/${testApiKey}`),
+ );
+ expect(result).toContain(
+ http(
+ `https://api.developer.coinbase.com/rpc/v1/base-sepolia/${testApiKey}`,
+ ),
+ );
+ });
+});
diff --git a/src/createWagmiConfig.ts b/src/createWagmiConfig.ts
new file mode 100644
index 0000000000..777045afa1
--- /dev/null
+++ b/src/createWagmiConfig.ts
@@ -0,0 +1,37 @@
+import { http, cookieStorage, createConfig, createStorage } from 'wagmi';
+import { base, baseSepolia } from 'wagmi/chains';
+import { coinbaseWallet } from 'wagmi/connectors';
+import type { CreateWagmiConfigParams } from './types';
+
+// createWagmiConfig returns a WagmiConfig (https://wagmi.sh/react/api/createConfig) using OnchainKit provided settings.
+// This function should only be used if the user does not provide WagmiProvider as a parent in the React context.
+export const createWagmiConfig = ({
+ apiKey,
+ appName,
+ appLogoUrl,
+}: CreateWagmiConfigParams) => {
+ return createConfig({
+ chains: [base, baseSepolia],
+ connectors: [
+ coinbaseWallet({
+ appName,
+ appLogoUrl,
+ preference: 'all',
+ }),
+ ],
+ storage: createStorage({
+ storage: cookieStorage,
+ }),
+ ssr: true,
+ transports: {
+ [base.id]: apiKey
+ ? http(`https://api.developer.coinbase.com/rpc/v1/base/${apiKey}`)
+ : http(),
+ [baseSepolia.id]: apiKey
+ ? http(
+ `https://api.developer.coinbase.com/rpc/v1/base-sepolia/${apiKey}`,
+ )
+ : http(),
+ },
+ });
+};
diff --git a/src/types.ts b/src/types.ts
index 4711a0bfc3..6487a85091 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -15,6 +15,12 @@ export type AppConfig = {
paymaster?: string | null; // Paymaster URL for gas sponsorship
};
+export type CreateWagmiConfigParams = {
+ apiKey?: string;
+ appName?: string;
+ appLogoUrl?: string;
+};
+
/**
* Note: exported as public Type
*/
diff --git a/src/useProviderDependencies.test.tsx b/src/useProviderDependencies.test.tsx
new file mode 100644
index 0000000000..4c606477a3
--- /dev/null
+++ b/src/useProviderDependencies.test.tsx
@@ -0,0 +1,110 @@
+import { type QueryClient, useQueryClient } from '@tanstack/react-query';
+import { renderHook } from '@testing-library/react';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { type Config, WagmiProviderNotFoundError, useConfig } from 'wagmi';
+import { useProviderDependencies } from './useProviderDependencies';
+
+// Mock the wagmi and react-query hooks
+vi.mock('wagmi', async () => {
+ const actual = await vi.importActual('wagmi');
+ return {
+ ...actual,
+ useConfig: vi.fn(),
+ };
+});
+
+vi.mock('@tanstack/react-query', async () => {
+ const actual = await vi.importActual('@tanstack/react-query');
+ return {
+ ...actual,
+ useQueryClient: vi.fn(),
+ };
+});
+
+describe('useProviderDependencies', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should return both configs when they exist', () => {
+ const mockWagmiConfig = { testWagmi: true } as unknown as Config;
+ const mockQueryClient = { testQuery: true } as unknown as QueryClient;
+ vi.mocked(useConfig).mockReturnValue(mockWagmiConfig);
+ vi.mocked(useQueryClient).mockReturnValue(mockQueryClient);
+ const { result } = renderHook(() => useProviderDependencies());
+ expect(result.current).toEqual({
+ providedWagmiConfig: mockWagmiConfig,
+ providedQueryClient: mockQueryClient,
+ });
+ });
+
+ it('should handle missing WagmiProvider gracefully', () => {
+ vi.mocked(useConfig).mockImplementation(() => {
+ throw new WagmiProviderNotFoundError();
+ });
+ const mockQueryClient = { testQuery: true } as unknown as QueryClient;
+ vi.mocked(useQueryClient).mockReturnValue(mockQueryClient);
+ const { result } = renderHook(() => useProviderDependencies());
+ expect(result.current).toEqual({
+ providedWagmiConfig: null,
+ providedQueryClient: mockQueryClient,
+ });
+ });
+
+ it('should handle missing QueryClient gracefully', () => {
+ const mockWagmiConfig = { testWagmi: true } as unknown as Config;
+ vi.mocked(useConfig).mockReturnValue(mockWagmiConfig);
+ vi.mocked(useQueryClient).mockImplementation(() => {
+ throw new Error('No QueryClient set, use QueryClientProvider to set one');
+ });
+ const { result } = renderHook(() => useProviderDependencies());
+ expect(result.current).toEqual({
+ providedWagmiConfig: mockWagmiConfig,
+ providedQueryClient: null,
+ });
+ });
+
+ it('should log non-WagmiProvider errors and return null config', () => {
+ const consoleErrorSpy = vi
+ .spyOn(console, 'error')
+ .mockImplementation(() => {});
+ const error = new Error('Different error');
+ vi.mocked(useConfig).mockImplementation(() => {
+ throw error;
+ });
+ const mockQueryClient = { testQuery: true } as unknown as QueryClient;
+ vi.mocked(useQueryClient).mockReturnValue(mockQueryClient);
+ const { result } = renderHook(() => useProviderDependencies());
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
+ 'Error fetching WagmiProvider, using default:',
+ error,
+ );
+ expect(result.current).toEqual({
+ providedWagmiConfig: null,
+ providedQueryClient: mockQueryClient,
+ });
+ consoleErrorSpy.mockRestore();
+ });
+
+ it('should log non-QueryClient provider errors and return null client', () => {
+ const consoleErrorSpy = vi
+ .spyOn(console, 'error')
+ .mockImplementation(() => {});
+ const mockWagmiConfig = { testWagmi: true } as unknown as Config;
+ vi.mocked(useConfig).mockReturnValue(mockWagmiConfig);
+ const error = new Error('Different query error');
+ vi.mocked(useQueryClient).mockImplementation(() => {
+ throw error;
+ });
+ const { result } = renderHook(() => useProviderDependencies());
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
+ 'Error fetching QueryClient, using default:',
+ error,
+ );
+ expect(result.current).toEqual({
+ providedWagmiConfig: mockWagmiConfig,
+ providedQueryClient: null,
+ });
+ consoleErrorSpy.mockRestore();
+ });
+});
diff --git a/src/useProviderDependencies.tsx b/src/useProviderDependencies.tsx
new file mode 100644
index 0000000000..7bbd7a5224
--- /dev/null
+++ b/src/useProviderDependencies.tsx
@@ -0,0 +1,42 @@
+import { type QueryClient, useQueryClient } from '@tanstack/react-query';
+import { useMemo } from 'react';
+import { type Config, WagmiProviderNotFoundError, useConfig } from 'wagmi';
+
+// useProviderDependencies will return the provided Wagmi configuration and QueryClient if they exist in the React context, otherwise it will return null
+// NotFound errors will fail gracefully
+// Unexpected errors will be logged to the console as an error, and will return null for the problematic dependency
+export function useProviderDependencies() {
+ // Check the context for WagmiProvider
+ // Wagmi configuration defaults to the provided config if it exists
+ // Otherwise, use the OnchainKit-provided Wagmi configuration
+ let providedWagmiConfig: Config | null = null;
+ let providedQueryClient: QueryClient | null = null;
+
+ try {
+ providedWagmiConfig = useConfig();
+ } catch (error) {
+ if (!(error instanceof WagmiProviderNotFoundError)) {
+ console.error('Error fetching WagmiProvider, using default:', error);
+ }
+ }
+
+ try {
+ providedQueryClient = useQueryClient();
+ } catch (error) {
+ if (
+ !(
+ (error as Error).message ===
+ 'No QueryClient set, use QueryClientProvider to set one'
+ )
+ ) {
+ console.error('Error fetching QueryClient, using default:', error);
+ }
+ }
+
+ return useMemo(() => {
+ return {
+ providedWagmiConfig,
+ providedQueryClient,
+ };
+ }, [providedWagmiConfig, providedQueryClient]);
+}