Skip to content

Commit a27a2f9

Browse files
authored
feat: OnchainKitProvider default dependencies (#1589)
1 parent 3e7c9ad commit a27a2f9

7 files changed

+391
-5
lines changed

src/OnchainKitProvider.test.tsx

+52-3
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,38 @@ import '@testing-library/jest-dom';
22
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
33
import { render, screen, waitFor } from '@testing-library/react';
44
import { base } from 'viem/chains';
5-
import { describe, expect, it, vi } from 'vitest';
5+
import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest';
66
import { http, WagmiProvider, createConfig } from 'wagmi';
7+
import { useConfig } from 'wagmi';
78
import { mock } from 'wagmi/connectors';
89
import { setOnchainKitConfig } from './OnchainKitConfig';
910
import { OnchainKitProvider } from './OnchainKitProvider';
1011
import { COINBASE_VERIFIED_ACCOUNT_SCHEMA_ID } from './identity/constants';
1112
import type { EASSchemaUid } from './identity/types';
1213
import { useOnchainKit } from './useOnchainKit';
14+
import { useProviderDependencies } from './useProviderDependencies';
15+
16+
vi.mock('wagmi', async (importOriginal) => {
17+
const actual = await importOriginal();
18+
return {
19+
...actual,
20+
useConfig: vi.fn(),
21+
};
22+
});
23+
24+
vi.mock('./useProviderDependencies', () => ({
25+
useProviderDependencies: vi.fn(() => ({
26+
providedWagmiConfig: null,
27+
providedQueryClient: null,
28+
})),
29+
}));
30+
31+
vi.mock('./useProviderDependencies', () => ({
32+
useProviderDependencies: vi.fn(() => ({
33+
providedWagmiConfig: null,
34+
providedQueryClient: null,
35+
})),
36+
}));
1337

1438
const queryClient = new QueryClient();
1539
const mockConfig = createConfig({
@@ -51,8 +75,17 @@ describe('OnchainKitProvider', () => {
5175
const apiKey = 'test-api-key';
5276
const paymasterUrl =
5377
'https://api.developer.coinbase.com/rpc/v1/base/test-api-key';
54-
const appLogo = undefined;
55-
const appName = undefined;
78+
const appLogo = '';
79+
const appName = 'Dapp';
80+
81+
beforeEach(() => {
82+
vi.clearAllMocks();
83+
(useConfig as Mock).mockReturnValue(mockConfig);
84+
(useProviderDependencies as Mock).mockReturnValue({
85+
providedWagmiConfig: mockConfig,
86+
providedQueryClient: queryClient,
87+
});
88+
});
5689

5790
it('provides the context value correctly', async () => {
5891
render(
@@ -71,6 +104,22 @@ describe('OnchainKitProvider', () => {
71104
});
72105
});
73106

107+
it('provides the context value correctly without WagmiProvider', async () => {
108+
(useProviderDependencies as Mock).mockReturnValue({
109+
providedWagmiConfig: null,
110+
providedQueryClient: null,
111+
});
112+
render(
113+
<OnchainKitProvider chain={base} schemaId={schemaId} apiKey={apiKey}>
114+
<TestComponent />
115+
</OnchainKitProvider>,
116+
);
117+
await waitFor(() => {
118+
expect(screen.getByText(schemaId)).toBeInTheDocument();
119+
expect(screen.getByText(apiKey)).toBeInTheDocument();
120+
});
121+
});
122+
74123
it('throws an error if schemaId does not meet the required length', () => {
75124
expect(() => {
76125
render(

src/OnchainKitProvider.tsx

+48-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
12
import { createContext, useMemo } from 'react';
3+
import { WagmiProvider } from 'wagmi';
24
import { ONCHAIN_KIT_CONFIG, setOnchainKitConfig } from './OnchainKitConfig';
5+
import { createWagmiConfig } from './createWagmiConfig';
36
import { COINBASE_VERIFIED_ACCOUNT_SCHEMA_ID } from './identity/constants';
47
import { checkHashLength } from './internal/utils/checkHashLength';
58
import type { OnchainKitContextType, OnchainKitProviderReact } from './types';
9+
import { useProviderDependencies } from './useProviderDependencies';
610

711
export const OnchainKitContext =
812
createContext<OnchainKitContextType>(ONCHAIN_KIT_CONFIG);
@@ -24,6 +28,7 @@ export function OnchainKitProvider({
2428
throw Error('EAS schemaId must be 64 characters prefixed with "0x"');
2529
}
2630

31+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: ignore
2732
const value = useMemo(() => {
2833
const defaultPaymasterUrl = apiKey
2934
? `https://api.developer.coinbase.com/rpc/v1/${chain.name
@@ -36,8 +41,8 @@ export function OnchainKitProvider({
3641
chain: chain,
3742
config: {
3843
appearance: {
39-
name: config?.appearance?.name,
40-
logo: config?.appearance?.logo,
44+
name: config?.appearance?.name ?? 'Dapp',
45+
logo: config?.appearance?.logo ?? '',
4146
mode: config?.appearance?.mode ?? 'auto',
4247
theme: config?.appearance?.theme ?? 'default',
4348
},
@@ -51,6 +56,47 @@ export function OnchainKitProvider({
5156
return onchainKitConfig;
5257
}, [address, apiKey, chain, config, projectId, rpcUrl, schemaId]);
5358

59+
// Check the React context for WagmiProvider and QueryClientProvider
60+
const { providedWagmiConfig, providedQueryClient } =
61+
useProviderDependencies();
62+
63+
const defaultConfig = useMemo(() => {
64+
// IMPORTANT: Don't create a new Wagmi configuration if one already exists
65+
// This prevents the user-provided WagmiConfig from being overriden
66+
return (
67+
providedWagmiConfig ||
68+
createWagmiConfig({
69+
apiKey,
70+
appName: value.config.appearance.name,
71+
appLogoUrl: value.config.appearance.logo,
72+
})
73+
);
74+
}, [
75+
apiKey,
76+
providedWagmiConfig,
77+
value.config.appearance.name,
78+
value.config.appearance.logo,
79+
]);
80+
const defaultQueryClient = useMemo(() => {
81+
// IMPORTANT: Don't create a new QueryClient if one already exists
82+
// This prevents the user-provided QueryClient from being overriden
83+
return providedQueryClient || new QueryClient();
84+
}, [providedQueryClient]);
85+
86+
// If both dependencies are missing, return a context with default parent providers
87+
// If only one dependency is provided, expect the user to also provide the missing one
88+
if (!providedWagmiConfig && !providedQueryClient) {
89+
return (
90+
<WagmiProvider config={defaultConfig}>
91+
<QueryClientProvider client={defaultQueryClient}>
92+
<OnchainKitContext.Provider value={value}>
93+
{children}
94+
</OnchainKitContext.Provider>
95+
</QueryClientProvider>
96+
</WagmiProvider>
97+
);
98+
}
99+
54100
return (
55101
<OnchainKitContext.Provider value={value}>
56102
{children}

src/createWagmiConfig.test.ts

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
import { createConfig } from 'wagmi';
3+
import { http } from 'wagmi';
4+
import { base, baseSepolia } from 'wagmi/chains';
5+
import { coinbaseWallet } from 'wagmi/connectors';
6+
import { createWagmiConfig } from './createWagmiConfig';
7+
8+
// Mock the imported modules
9+
vi.mock('wagmi', async () => {
10+
const actual = await vi.importActual('wagmi');
11+
return {
12+
...actual,
13+
createConfig: vi.fn(),
14+
createStorage: vi.fn(),
15+
};
16+
});
17+
18+
vi.mock('wagmi/chains', async () => {
19+
const actual = await vi.importActual('wagmi/chains');
20+
return {
21+
...actual,
22+
base: { id: 8453 },
23+
baseSepolia: { id: 84532 },
24+
};
25+
});
26+
27+
vi.mock('wagmi/connectors', async () => {
28+
const actual = await vi.importActual('wagmi/connectors');
29+
return {
30+
...actual,
31+
coinbaseWallet: vi.fn(),
32+
};
33+
});
34+
35+
describe('createWagmiConfig', () => {
36+
it('should create config with default values when no parameters are provided', () => {
37+
createWagmiConfig({});
38+
expect(createConfig).toHaveBeenCalledWith(
39+
expect.objectContaining({
40+
chains: [base, baseSepolia],
41+
ssr: true,
42+
transports: {
43+
[base.id]: expect.any(Function),
44+
[baseSepolia.id]: expect.any(Function),
45+
},
46+
}),
47+
);
48+
expect(coinbaseWallet).toHaveBeenCalledWith({
49+
appName: undefined,
50+
appLogoUrl: undefined,
51+
preference: 'all',
52+
});
53+
});
54+
55+
it('should create config with custom values when parameters are provided', () => {
56+
const customConfig = {
57+
appearance: {
58+
name: 'Custom App',
59+
logo: 'https://example.com/logo.png',
60+
},
61+
};
62+
createWagmiConfig({
63+
apiKey: 'test-api-key',
64+
appName: customConfig.appearance.name,
65+
appLogoUrl: customConfig.appearance.logo,
66+
});
67+
expect(createConfig).toHaveBeenCalledWith(
68+
expect.objectContaining({
69+
chains: [base, baseSepolia],
70+
ssr: true,
71+
transports: {
72+
[base.id]: expect.any(Function),
73+
[baseSepolia.id]: expect.any(Function),
74+
},
75+
}),
76+
);
77+
expect(coinbaseWallet).toHaveBeenCalledWith({
78+
appName: 'Custom App',
79+
appLogoUrl: 'https://example.com/logo.png',
80+
preference: 'all',
81+
});
82+
});
83+
84+
it('should use API key in transports when provided', () => {
85+
const testApiKey = 'test-api-key';
86+
const result = createWagmiConfig({ apiKey: testApiKey });
87+
expect(result).toContain(
88+
http(`https://api.developer.coinbase.com/rpc/v1/base/${testApiKey}`),
89+
);
90+
expect(result).toContain(
91+
http(
92+
`https://api.developer.coinbase.com/rpc/v1/base-sepolia/${testApiKey}`,
93+
),
94+
);
95+
});
96+
});

src/createWagmiConfig.ts

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { http, cookieStorage, createConfig, createStorage } from 'wagmi';
2+
import { base, baseSepolia } from 'wagmi/chains';
3+
import { coinbaseWallet } from 'wagmi/connectors';
4+
import type { CreateWagmiConfigParams } from './types';
5+
6+
// createWagmiConfig returns a WagmiConfig (https://wagmi.sh/react/api/createConfig) using OnchainKit provided settings.
7+
// This function should only be used if the user does not provide WagmiProvider as a parent in the React context.
8+
export const createWagmiConfig = ({
9+
apiKey,
10+
appName,
11+
appLogoUrl,
12+
}: CreateWagmiConfigParams) => {
13+
return createConfig({
14+
chains: [base, baseSepolia],
15+
connectors: [
16+
coinbaseWallet({
17+
appName,
18+
appLogoUrl,
19+
preference: 'all',
20+
}),
21+
],
22+
storage: createStorage({
23+
storage: cookieStorage,
24+
}),
25+
ssr: true,
26+
transports: {
27+
[base.id]: apiKey
28+
? http(`https://api.developer.coinbase.com/rpc/v1/base/${apiKey}`)
29+
: http(),
30+
[baseSepolia.id]: apiKey
31+
? http(
32+
`https://api.developer.coinbase.com/rpc/v1/base-sepolia/${apiKey}`,
33+
)
34+
: http(),
35+
},
36+
});
37+
};

src/types.ts

+6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ export type AppConfig = {
1515
paymaster?: string | null; // Paymaster URL for gas sponsorship
1616
};
1717

18+
export type CreateWagmiConfigParams = {
19+
apiKey?: string;
20+
appName?: string;
21+
appLogoUrl?: string;
22+
};
23+
1824
/**
1925
* Note: exported as public Type
2026
*/

0 commit comments

Comments
 (0)