Skip to content

Commit 160afc1

Browse files
committed
feat: add ConfigContext infrastructure for instance-scoped configuration
Introduces React Context pattern for config access, enabling future support for multiple WormholeConnect instances on the same page. Changes: - Add ConfigContext with ConfigProvider and useConfig() hook - Wrap WormholeConnect with ConfigProvider - Update AppRouter to use useConfig() (example migration) - Add TestConfigContext pattern in testHelpers for test isolation - Mark singleton export with @deprecated JSDoc - Fix TypeScript dev server errors (@types/color, env.d.ts) The singleton remains functional for backwards compatibility. Migration can proceed incrementally file-by-file.
1 parent 0e29327 commit 160afc1

7 files changed

Lines changed: 318 additions & 82 deletions

File tree

src/AppRouter.tsx

Lines changed: 20 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1-
import React, { useCallback, useContext, useEffect, useRef } from 'react';
1+
import React, { useContext, useEffect, useRef } from 'react';
22
import { useDispatch, useSelector } from 'react-redux';
33
import { useTheme } from '@mui/material/styles';
44

55
import './App.css';
66
import type { RootState } from './store';
77
import { clearRedeem } from './store/redeem';
88
import { clearTransfer } from './store/transferInput';
9-
import { isEmptyObject, usePrevious } from './utils';
10-
import type { WormholeConnectConfig } from './config/types';
11-
import { setConfig } from './config';
12-
import config from './config';
9+
import { usePrevious } from './utils';
10+
import { useConfig } from './contexts/ConfigContext';
1311

1412
import Terms from './views/Terms';
1513
import TxSearch from './views/TxSearch';
@@ -80,56 +78,32 @@ const AppRouterContent = () => {
8078
);
8179
};
8280

83-
interface Props {
84-
config?: WormholeConnectConfig;
85-
}
86-
8781
// since this will be embedded, we'll have to use pseudo routes instead of relying on the url
88-
function AppRouter(props: Props) {
82+
function AppRouter() {
8983
const dispatch = useDispatch();
90-
91-
const hasSetSsgConfig = useRef(false);
92-
const isInitialLoad = useRef(true);
84+
const config = useConfig();
9385
const route = useSelector((state: RootState) => state.router.route);
9486

95-
const loadConfig = useCallback((customConfig: WormholeConnectConfig) => {
96-
if (!isEmptyObject(customConfig)) {
97-
setConfig(customConfig);
98-
}
99-
100-
hasSetSsgConfig.current = true;
101-
config.triggerEvent({
102-
type: 'config',
103-
config: customConfig,
104-
});
105-
}, []);
87+
// Track config changes to clear transfer state when config is updated
88+
const prevConfig = usePrevious(config);
89+
const hasInitialized = useRef(false);
10690

107-
if (!hasSetSsgConfig.current) {
108-
// This runs once in SSG step (server-side pre-rendering)
109-
if (props.config) {
110-
loadConfig(props.config);
111-
}
112-
if (route !== 'bridge') {
113-
// The route may not be bridge on initial load if the component was re-rendered after client side navigation
114-
dispatch(setRoute('bridge'));
91+
// SSG route initialization - ensure we start on bridge route
92+
useEffect(() => {
93+
if (!hasInitialized.current) {
94+
hasInitialized.current = true;
95+
if (route !== 'bridge') {
96+
dispatch(setRoute('bridge'));
97+
}
11598
}
116-
}
99+
}, [route, dispatch]);
117100

101+
// Clear transfer state when config changes (after initial load)
118102
useEffect(() => {
119-
if (isInitialLoad.current) {
120-
isInitialLoad.current = false;
121-
config.triggerEvent({
122-
type: 'load',
123-
config: props.config,
124-
});
125-
} else {
126-
if (props.config) {
127-
loadConfig(props.config);
128-
dispatch(clearTransfer());
129-
}
103+
if (prevConfig && prevConfig !== config) {
104+
dispatch(clearTransfer());
130105
}
131-
}, [props.config, loadConfig, dispatch]);
132-
// END config loading code
106+
}, [config, prevConfig, dispatch]);
133107

134108
return <AppRouterContent />;
135109
}

src/WormholeConnect.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type { WormholeConnectTheme } from 'theme';
1212
import { RouteProvider } from './contexts/RouteContext';
1313
import { TokensProvider } from './contexts/TokensContext';
1414
import WalletProvider from './contexts/wallet/WalletProvider';
15+
import { ConfigProvider } from './contexts/ConfigContext';
1516
import type { WormholeConnectWalletProvider } from './utils/wallet/types';
1617
import { internalWalletProvider } from './utils/wallet/InternalWalletProvider';
1718

@@ -56,13 +57,15 @@ export default function WormholeConnect({
5657
<ThemeProvider theme={muiTheme}>
5758
<ScopedCssBaseline enableColorScheme>
5859
<ErrorBoundary>
59-
<WalletProvider provider={walletProvider}>
60-
<TokensProvider>
61-
<RouteProvider>
62-
<AppRouter config={config} />
63-
</RouteProvider>
64-
</TokensProvider>
65-
</WalletProvider>
60+
<ConfigProvider config={config}>
61+
<WalletProvider provider={walletProvider}>
62+
<TokensProvider>
63+
<RouteProvider>
64+
<AppRouter />
65+
</RouteProvider>
66+
</TokensProvider>
67+
</WalletProvider>
68+
</ConfigProvider>
6669
</ErrorBoundary>
6770
</ScopedCssBaseline>
6871
</ThemeProvider>

src/config/index.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,31 @@ export function buildConfig(
171171

172172
// Running buildConfig with no argument generates the default configuration
173173
const config = buildConfig();
174+
175+
/**
176+
* @deprecated Direct import of config singleton is deprecated.
177+
*
178+
* For React components/hooks: Use useConfig() to read, useSetConfig() to update.
179+
* For non-React code: Pass config as a parameter from the calling component.
180+
*
181+
* Migration:
182+
* ```tsx
183+
* // Before (deprecated)
184+
* import config from 'config';
185+
* const network = config.network;
186+
*
187+
* // After - in React components
188+
* import { useConfig, useSetConfig } from 'contexts/ConfigContext';
189+
* function MyComponent() {
190+
* const config = useConfig(); // read
191+
* const setConfig = useSetConfig(); // update
192+
* }
193+
*
194+
* // After - in utility functions
195+
* function myUtility(config: InternalConfig) { ... }
196+
* // Call from component: myUtility(config);
197+
* ```
198+
*/
174199
export default config;
175200

176201
export async function getWormholeContextV2(): Promise<WormholeV2<Network>> {
@@ -214,8 +239,27 @@ export async function newWormholeContextV2(): Promise<WormholeV2<Network>> {
214239
);
215240
}
216241

217-
// setConfig can be called afterwards to override the default config with integrator-provided config
218-
242+
/**
243+
* @deprecated setConfig() is deprecated.
244+
*
245+
* For React components: Use useSetConfig() hook from 'contexts/ConfigContext'.
246+
* This function mutates the global singleton which prevents multiple
247+
* WormholeConnect instances from having independent configurations.
248+
*
249+
* Migration:
250+
* ```tsx
251+
* // Before (deprecated)
252+
* import { setConfig } from 'config';
253+
* setConfig({ network: 'Testnet' });
254+
*
255+
* // After
256+
* import { useSetConfig } from 'contexts/ConfigContext';
257+
* function MyComponent() {
258+
* const setConfig = useSetConfig();
259+
* setConfig({ network: 'Testnet' });
260+
* }
261+
* ```
262+
*/
219263
export function setConfig(customConfig: WormholeConnectConfig = {}) {
220264
const newConfig: InternalConfig<Network> = buildConfig(customConfig);
221265

src/contexts/ConfigContext.tsx

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import * as React from 'react';
2+
import type { Network } from '@wormhole-foundation/sdk';
3+
import type { WormholeConnectConfig, InternalConfig } from '../config/types';
4+
import { buildConfig, setConfig } from '../config';
5+
import config from '../config';
6+
7+
export type SetConfigFn = (customConfig?: WormholeConnectConfig) => void;
8+
9+
export interface ConfigContextType {
10+
config: InternalConfig<Network>;
11+
setConfig: SetConfigFn;
12+
}
13+
14+
const ConfigContext = React.createContext<ConfigContextType | null>(null);
15+
16+
export interface ConfigProviderProps {
17+
config?: WormholeConnectConfig;
18+
children: React.ReactNode;
19+
}
20+
21+
/**
22+
* ConfigProvider builds and provides instance-scoped configuration.
23+
*
24+
* During the migration period, this provider also updates the global singleton
25+
* via setConfig() for backwards compatibility. Code that directly imports
26+
* `config` from 'config' will continue to work.
27+
*
28+
* TODO: Remove global singleton sync once all code uses useConfig()/useSetConfig().
29+
*/
30+
export const ConfigProvider: React.FC<ConfigProviderProps> = ({
31+
config: userConfig,
32+
children,
33+
}) => {
34+
// Track the current user config in state so useSetConfig can update it
35+
const [currentUserConfig, setCurrentUserConfig] = React.useState(userConfig);
36+
37+
// Build instance-specific config from user config
38+
// Memoize to prevent rebuilding on every render
39+
const internalConfig = React.useMemo(() => {
40+
// Build the config for this instance
41+
const builtConfig = buildConfig(currentUserConfig);
42+
43+
// Also update global singleton for backwards compatibility
44+
// This ensures files that directly import config still work
45+
// TODO: Remove when singleton is eliminated
46+
if (currentUserConfig) {
47+
setConfig(currentUserConfig);
48+
}
49+
50+
return builtConfig;
51+
}, [currentUserConfig]);
52+
53+
// Setter that updates both context state AND global singleton
54+
const updateConfig = React.useCallback(
55+
(customConfig?: WormholeConnectConfig) => {
56+
// Update local state (triggers re-render and useMemo rebuild)
57+
setCurrentUserConfig(customConfig);
58+
59+
// Also update global singleton for code that imports config directly
60+
// TODO: Remove when singleton is eliminated
61+
setConfig(customConfig);
62+
},
63+
[],
64+
);
65+
66+
// Trigger events on config changes (mirrors AppRouter behavior)
67+
const hasInitialized = React.useRef(false);
68+
React.useEffect(() => {
69+
if (!hasInitialized.current) {
70+
hasInitialized.current = true;
71+
config.triggerEvent({
72+
type: 'load',
73+
config: currentUserConfig,
74+
});
75+
} else {
76+
config.triggerEvent({
77+
type: 'config',
78+
config: currentUserConfig,
79+
});
80+
}
81+
}, [currentUserConfig]);
82+
83+
// Memoize the context value to prevent unnecessary re-renders
84+
const contextValue = React.useMemo(
85+
() => ({ config: internalConfig, setConfig: updateConfig }),
86+
[internalConfig, updateConfig],
87+
);
88+
89+
return (
90+
<ConfigContext.Provider value={contextValue}>
91+
{children}
92+
</ConfigContext.Provider>
93+
);
94+
};
95+
96+
/**
97+
* useConfig returns the instance-scoped configuration.
98+
*
99+
* This hook must be used within a ConfigProvider. It returns the InternalConfig
100+
* object built from the user-provided WormholeConnectConfig.
101+
*
102+
* @example
103+
* ```tsx
104+
* function MyComponent() {
105+
* const config = useConfig();
106+
* return <div>Network: {config.network}</div>;
107+
* }
108+
* ```
109+
*
110+
* @throws Error if used outside of ConfigProvider
111+
*/
112+
export function useConfig(): InternalConfig<Network> {
113+
const contextValue = React.useContext(ConfigContext);
114+
115+
if (contextValue === null) {
116+
throw new Error(
117+
'useConfig must be used within a ConfigProvider. ' +
118+
'Ensure your component is wrapped in <WormholeConnect> or <ConfigProvider>.',
119+
);
120+
}
121+
122+
return contextValue.config;
123+
}
124+
125+
/**
126+
* useSetConfig returns a function to update the configuration.
127+
*
128+
* The setter updates both the context state AND the global singleton,
129+
* ensuring backwards compatibility with code that imports config directly.
130+
*
131+
* @example
132+
* ```tsx
133+
* function MyComponent() {
134+
* const setConfig = useSetConfig();
135+
*
136+
* const handleNetworkChange = () => {
137+
* setConfig({ network: 'Testnet' });
138+
* };
139+
*
140+
* return <button onClick={handleNetworkChange}>Switch to Testnet</button>;
141+
* }
142+
* ```
143+
*
144+
* @throws Error if used outside of ConfigProvider
145+
*/
146+
export function useSetConfig(): SetConfigFn {
147+
const contextValue = React.useContext(ConfigContext);
148+
149+
if (contextValue === null) {
150+
throw new Error(
151+
'useSetConfig must be used within a ConfigProvider. ' +
152+
'Ensure your component is wrapped in <WormholeConnect> or <ConfigProvider>.',
153+
);
154+
}
155+
156+
return contextValue.setConfig;
157+
}
158+
159+
export { ConfigContext };

src/env.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
interface ImportMetaEnv {
44
// env
55
REACT_APP_CONNECT_ENV: string;
6+
REACT_APP_CONNECT_VERSION?: string;
7+
REACT_APP_CONNECT_GIT_HASH?: string;
68

79
// wallet connect
810
REACT_APP_WALLET_CONNECT_PROJECT_ID: string;
@@ -66,6 +68,14 @@ interface ImportMetaEnv {
6668
REACT_APP_PLUME_TESTNET_RPC: string;
6769
REACT_APP_XRPLEVM_TESTNET_RPC: string;
6870
REACT_APP_MONAD_TESTNET_RPC: string;
71+
REACT_APP_LINEA_TESTNET_RPC?: string;
72+
REACT_APP_SONIC_TESTNET_RPC?: string;
73+
REACT_APP_INK_TESTNET_RPC?: string;
74+
75+
// test private keys (dev only)
76+
REACT_APP_TEST_EVM_PK?: string;
77+
REACT_APP_SOL_PRIVATE_KEY?: string;
78+
REACT_APP_SUI_PRIVATE_KEY?: string;
6979

7080
// devnet
7181
REACT_APP_ETHEREUM_DEVNET_RPC: string;

src/utils/testHelpers.ts

Lines changed: 0 additions & 27 deletions
This file was deleted.

0 commit comments

Comments
 (0)