@openzeppelin/ui-react 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,864 @@
1
+ import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
2
+ import { AnalyticsService, cn, isRecordWithProperties, logger } from "@openzeppelin/ui-utils";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { Button } from "@openzeppelin/ui-components";
5
+
6
+ //#region src/hooks/AdapterContext.tsx
7
+ /**
8
+ * AdapterContext.tsx
9
+ *
10
+ * This file defines the React Context used for the adapter singleton pattern.
11
+ * It provides types and the context definition, but the actual implementation
12
+ * is in the AdapterProvider component.
13
+ *
14
+ * The adapter singleton pattern ensures that only one adapter instance exists
15
+ * per network configuration, which is critical for consistent wallet connection
16
+ * state across the application.
17
+ */
18
+ /**
19
+ * The React Context that provides adapter registry access throughout the app
20
+ * Components can access this through the useAdapterContext hook
21
+ */
22
+ const AdapterContext = createContext(null);
23
+
24
+ //#endregion
25
+ //#region src/hooks/AdapterProvider.tsx
26
+ /**
27
+ * AdapterProvider.tsx
28
+ *
29
+ * This file implements the Adapter Provider component which manages a registry of
30
+ * adapter instances. It's a key part of the adapter singleton pattern which ensures
31
+ * that only one adapter instance exists per network configuration.
32
+ *
33
+ * The adapter registry is shared across the application via React Context, allowing
34
+ * components to access the same adapter instances and maintain consistent wallet
35
+ * connection state.
36
+ *
37
+ * IMPORTANT: This implementation needs special care to avoid React state update errors
38
+ * during component rendering. Direct state updates during render are not allowed, which
39
+ * is why adapter loading is controlled carefully.
40
+ */
41
+ /**
42
+ * Provider component that manages adapter instances centrally
43
+ * to avoid creating multiple instances of the same adapter.
44
+ *
45
+ * This component:
46
+ * 1. Maintains a registry of adapter instances by network ID
47
+ * 2. Tracks loading states for adapters being initialized
48
+ * 3. Provides a function to get or load adapters for specific networks
49
+ * 4. Ensures adapter instances are reused when possible
50
+ */
51
+ function AdapterProvider({ children, resolveAdapter }) {
52
+ const [adapterRegistry, setAdapterRegistry] = useState({});
53
+ const [loadingNetworks, setLoadingNetworks] = useState(/* @__PURE__ */ new Set());
54
+ useEffect(() => {
55
+ const adapterCount = Object.keys(adapterRegistry).length;
56
+ if (adapterCount > 0) logger.info("AdapterProvider", `Registry contains ${adapterCount} adapters:`, {
57
+ networkIds: Object.keys(adapterRegistry),
58
+ loadingCount: loadingNetworks.size,
59
+ loadingNetworkIds: Array.from(loadingNetworks)
60
+ });
61
+ }, [adapterRegistry, loadingNetworks]);
62
+ /**
63
+ * Function to get or create an adapter for a network
64
+ *
65
+ * IMPORTANT: The actual adapter loading is handled in the useConfiguredAdapterSingleton hook
66
+ * to avoid React state updates during render, which would cause errors.
67
+ *
68
+ * This function:
69
+ * 1. Returns existing adapters immediately if available
70
+ * 2. Reports loading state for adapters being initialized
71
+ * 3. Initiates adapter loading when needed
72
+ */
73
+ const getAdapterForNetwork = useCallback((networkConfig) => {
74
+ if (!networkConfig) return {
75
+ adapter: null,
76
+ isLoading: false
77
+ };
78
+ const networkId = networkConfig.id;
79
+ logger.debug("AdapterProvider", `Adapter requested for network ${networkId}`);
80
+ if (adapterRegistry[networkId]) {
81
+ logger.debug("AdapterProvider", `Using existing adapter for network ${networkId}`);
82
+ return {
83
+ adapter: adapterRegistry[networkId],
84
+ isLoading: false
85
+ };
86
+ }
87
+ if (loadingNetworks.has(networkId)) {
88
+ logger.debug("AdapterProvider", `Adapter for network ${networkId} is currently loading`);
89
+ return {
90
+ adapter: null,
91
+ isLoading: true
92
+ };
93
+ }
94
+ setLoadingNetworks((prev) => {
95
+ const newSet = new Set(prev);
96
+ newSet.add(networkId);
97
+ return newSet;
98
+ });
99
+ logger.info("AdapterProvider", `Starting adapter initialization for network ${networkId} (${networkConfig.name})`);
100
+ resolveAdapter(networkConfig).then((adapter) => {
101
+ logger.info("AdapterProvider", `Adapter for network ${networkId} loaded successfully`, {
102
+ type: adapter.constructor.name,
103
+ objectId: Object.prototype.toString.call(adapter)
104
+ });
105
+ setAdapterRegistry((prev) => ({
106
+ ...prev,
107
+ [networkId]: adapter
108
+ }));
109
+ setLoadingNetworks((prev) => {
110
+ const newSet = new Set(prev);
111
+ newSet.delete(networkId);
112
+ return newSet;
113
+ });
114
+ }).catch((error) => {
115
+ logger.error("AdapterProvider", `Error loading adapter for network ${networkId}:`, error);
116
+ setLoadingNetworks((prev) => {
117
+ const newSet = new Set(prev);
118
+ newSet.delete(networkId);
119
+ return newSet;
120
+ });
121
+ });
122
+ return {
123
+ adapter: null,
124
+ isLoading: true
125
+ };
126
+ }, [
127
+ adapterRegistry,
128
+ loadingNetworks,
129
+ resolveAdapter
130
+ ]);
131
+ const contextValue = useMemo(() => ({ getAdapterForNetwork }), [getAdapterForNetwork]);
132
+ return /* @__PURE__ */ jsx(AdapterContext.Provider, {
133
+ value: contextValue,
134
+ children
135
+ });
136
+ }
137
+
138
+ //#endregion
139
+ //#region src/hooks/WalletStateContext.tsx
140
+ const WalletStateContext = createContext(void 0);
141
+ /**
142
+ * Hook to access wallet state from WalletStateProvider.
143
+ * @throws Error if used outside of WalletStateProvider
144
+ */
145
+ function useWalletState() {
146
+ const context = React.useContext(WalletStateContext);
147
+ if (context === void 0) throw new Error("useWalletState must be used within a WalletStateProvider");
148
+ return context;
149
+ }
150
+
151
+ //#endregion
152
+ //#region src/hooks/useAdapterContext.ts
153
+ /**
154
+ * useAdapterContext.ts
155
+ *
156
+ * This file provides a hook to access the AdapterContext throughout the application.
157
+ * It's a critical part of the adapter singleton pattern, allowing components to
158
+ * access the centralized adapter registry.
159
+ *
160
+ * The adapter singleton pattern ensures:
161
+ * - Only one adapter instance exists per network
162
+ * - Wallet connection state is consistent across the app
163
+ * - Better performance by eliminating redundant adapter initialization
164
+ */
165
+ /**
166
+ * Hook to access the adapter context
167
+ *
168
+ * This hook provides access to the getAdapterForNetwork function which
169
+ * retrieves or creates adapter instances from the singleton registry.
170
+ *
171
+ * Components should typically use useConfiguredAdapterSingleton instead
172
+ * of this hook directly, as it handles React state update timing properly.
173
+ *
174
+ * @throws Error if used outside of an AdapterProvider context
175
+ * @returns The adapter context value
176
+ */
177
+ function useAdapterContext() {
178
+ const context = useContext(AdapterContext);
179
+ if (!context) throw new Error("useAdapterContext must be used within an AdapterProvider");
180
+ return context;
181
+ }
182
+
183
+ //#endregion
184
+ //#region src/hooks/WalletStateProvider.tsx
185
+ /**
186
+ * Configures the adapter's UI kit and returns the UI provider component and hooks.
187
+ */
188
+ async function configureAdapterUiKit(adapter, loadConfigModule, programmaticOverrides = {}) {
189
+ try {
190
+ if (typeof adapter.configureUiKit === "function") {
191
+ logger.info("[WSP configureAdapterUiKit] Calling configureUiKit for adapter:", adapter?.networkConfig?.id);
192
+ await adapter.configureUiKit(programmaticOverrides, { loadUiKitNativeConfig: loadConfigModule });
193
+ logger.info("[WSP configureAdapterUiKit] configureUiKit completed for adapter:", adapter?.networkConfig?.id);
194
+ }
195
+ const providerComponent = adapter.getEcosystemReactUiContextProvider?.() || null;
196
+ const hooks = adapter.getEcosystemReactHooks?.() || null;
197
+ logger.info("[WSP configureAdapterUiKit]", "UI provider and hooks retrieved successfully.");
198
+ return {
199
+ providerComponent,
200
+ hooks
201
+ };
202
+ } catch (error) {
203
+ logger.error("[WSP configureAdapterUiKit]", "Error during adapter UI setup:", error);
204
+ throw error;
205
+ }
206
+ }
207
+ /**
208
+ * @name WalletStateProvider
209
+ * @description This provider is a central piece of the application's state management for wallet and network interactions.
210
+ * It is responsible for:
211
+ * 1. Managing the globally selected active network ID (`activeNetworkId`).
212
+ * 2. Deriving the full `NetworkConfig` object (`activeNetworkConfig`) for the active network.
213
+ * 3. Fetching and providing the corresponding `ContractAdapter` instance (`activeAdapter`) for the active network,
214
+ * leveraging the `AdapterProvider` to ensure adapter singletons.
215
+ * 4. Storing and providing the `EcosystemSpecificReactHooks` (`walletFacadeHooks`) from the active adapter.
216
+ * 5. Rendering the adapter-specific UI context provider (e.g., WagmiProvider for EVM) around its children,
217
+ * which is essential for the facade hooks to function correctly.
218
+ * 6. Providing a function (`setActiveNetworkId`) to change the globally active network.
219
+ *
220
+ * Consumers use the `useWalletState()` hook to access this global state.
221
+ * It should be placed high in the component tree, inside an `<AdapterProvider>`.
222
+ */
223
+ function WalletStateProvider({ children, initialNetworkId = null, getNetworkConfigById, loadConfigModule }) {
224
+ const [currentGlobalNetworkId, setCurrentGlobalNetworkIdState] = useState(initialNetworkId);
225
+ const [currentGlobalNetworkConfig, setCurrentGlobalNetworkConfig] = useState(null);
226
+ const [globalActiveAdapter, setGlobalActiveAdapter] = useState(null);
227
+ const [isGlobalAdapterLoading, setIsGlobalAdapterLoading] = useState(false);
228
+ const [walletFacadeHooks, setWalletFacadeHooks] = useState(null);
229
+ const [AdapterUiContextProviderToRender, setAdapterUiContextProviderToRender] = useState(null);
230
+ const [uiKitConfigVersion, setUiKitConfigVersion] = useState(0);
231
+ const [programmaticUiKitConfig, setProgrammaticUiKitConfig] = useState(void 0);
232
+ const { getAdapterForNetwork } = useAdapterContext();
233
+ useEffect(() => {
234
+ const abortController = new AbortController();
235
+ async function fetchNetworkConfig() {
236
+ if (!currentGlobalNetworkId) {
237
+ if (!abortController.signal.aborted) setCurrentGlobalNetworkConfig(null);
238
+ return;
239
+ }
240
+ try {
241
+ const config = await Promise.resolve(getNetworkConfigById(currentGlobalNetworkId));
242
+ if (!abortController.signal.aborted) setCurrentGlobalNetworkConfig(config || null);
243
+ } catch (error) {
244
+ if (!abortController.signal.aborted) {
245
+ logger.error("[WSP fetchNetworkConfig]", "Failed to fetch network config:", error);
246
+ setCurrentGlobalNetworkConfig(null);
247
+ }
248
+ }
249
+ }
250
+ fetchNetworkConfig();
251
+ return () => abortController.abort();
252
+ }, [currentGlobalNetworkId, getNetworkConfigById]);
253
+ useEffect(() => {
254
+ const abortController = new AbortController();
255
+ async function loadAdapterAndConfigureUi() {
256
+ if (!currentGlobalNetworkConfig) {
257
+ if (!abortController.signal.aborted) {
258
+ setGlobalActiveAdapter(null);
259
+ setIsGlobalAdapterLoading(false);
260
+ setAdapterUiContextProviderToRender(null);
261
+ setWalletFacadeHooks(null);
262
+ }
263
+ return;
264
+ }
265
+ const { adapter: newAdapter, isLoading: newIsLoading } = getAdapterForNetwork(currentGlobalNetworkConfig);
266
+ if (abortController.signal.aborted) return;
267
+ setIsGlobalAdapterLoading(newIsLoading);
268
+ if (newAdapter && !newIsLoading) try {
269
+ const { providerComponent, hooks } = await configureAdapterUiKit(newAdapter, loadConfigModule, programmaticUiKitConfig);
270
+ if (!abortController.signal.aborted) {
271
+ setAdapterUiContextProviderToRender(() => providerComponent);
272
+ setWalletFacadeHooks(hooks);
273
+ setGlobalActiveAdapter(newAdapter);
274
+ }
275
+ } catch (error) {
276
+ if (!abortController.signal.aborted) {
277
+ logger.error("[WSP loadAdapterAndConfigureUi]", "Error during adapter UI setup:", error);
278
+ setAdapterUiContextProviderToRender(null);
279
+ setWalletFacadeHooks(null);
280
+ }
281
+ }
282
+ else if (!newAdapter && !newIsLoading) {
283
+ if (!abortController.signal.aborted) {
284
+ setAdapterUiContextProviderToRender(null);
285
+ setWalletFacadeHooks(null);
286
+ setGlobalActiveAdapter(null);
287
+ }
288
+ }
289
+ }
290
+ loadAdapterAndConfigureUi();
291
+ return () => abortController.abort();
292
+ }, [
293
+ currentGlobalNetworkConfig,
294
+ getAdapterForNetwork,
295
+ loadConfigModule,
296
+ uiKitConfigVersion,
297
+ programmaticUiKitConfig
298
+ ]);
299
+ /**
300
+ * Callback to set the globally active network ID.
301
+ * Also clears dependent states (config, adapter, hooks) if the network ID is cleared.
302
+ */
303
+ const setActiveNetworkIdCallback = useCallback((networkId) => {
304
+ logger.info("WalletStateProvider", `Setting global network ID to: ${networkId}`);
305
+ setCurrentGlobalNetworkIdState(networkId);
306
+ if (!networkId) {
307
+ setCurrentGlobalNetworkConfig(null);
308
+ setGlobalActiveAdapter(null);
309
+ setIsGlobalAdapterLoading(false);
310
+ setWalletFacadeHooks(null);
311
+ }
312
+ }, []);
313
+ /**
314
+ * Callback to explicitly trigger a re-configuration of the active adapter's UI kit.
315
+ * This is useful when a UI kit setting changes (e.g., via a wizard) without a network change.
316
+ */
317
+ const reconfigureActiveAdapterUiKit = useCallback((uiKitConfig) => {
318
+ logger.info("WalletStateProvider", "Explicitly triggering UI kit re-configuration by bumping version.", uiKitConfig);
319
+ setProgrammaticUiKitConfig(uiKitConfig);
320
+ setUiKitConfigVersion((v) => v + 1);
321
+ }, [setProgrammaticUiKitConfig, setUiKitConfigVersion]);
322
+ const contextValue = useMemo(() => ({
323
+ activeNetworkId: currentGlobalNetworkId,
324
+ setActiveNetworkId: setActiveNetworkIdCallback,
325
+ activeNetworkConfig: currentGlobalNetworkConfig,
326
+ activeAdapter: globalActiveAdapter,
327
+ isAdapterLoading: isGlobalAdapterLoading,
328
+ walletFacadeHooks,
329
+ reconfigureActiveAdapterUiKit
330
+ }), [
331
+ currentGlobalNetworkId,
332
+ setActiveNetworkIdCallback,
333
+ currentGlobalNetworkConfig,
334
+ globalActiveAdapter,
335
+ isGlobalAdapterLoading,
336
+ walletFacadeHooks,
337
+ reconfigureActiveAdapterUiKit
338
+ ]);
339
+ const ActualProviderToRender = AdapterUiContextProviderToRender;
340
+ let childrenToRender;
341
+ if (ActualProviderToRender) {
342
+ const key = `${globalActiveAdapter?.networkConfig?.ecosystem || "unknown"}-${globalActiveAdapter?.networkConfig?.id || "unknown"}`;
343
+ logger.info("[WSP RENDER]", "Rendering adapter-provided UI context provider:", ActualProviderToRender.displayName || ActualProviderToRender.name || "UnknownComponent", "with key:", key);
344
+ childrenToRender = /* @__PURE__ */ jsx(ActualProviderToRender, { children }, key);
345
+ } else {
346
+ logger.info("[WSP RENDER]", "No adapter UI context provider to render. Rendering direct children.");
347
+ childrenToRender = children;
348
+ }
349
+ return /* @__PURE__ */ jsx(WalletStateContext.Provider, {
350
+ value: contextValue,
351
+ children: childrenToRender
352
+ });
353
+ }
354
+
355
+ //#endregion
356
+ //#region src/hooks/AnalyticsContext.tsx
357
+ /**
358
+ * Analytics context for providing analytics functionality to React components.
359
+ * Must be used within an AnalyticsProvider.
360
+ */
361
+ const AnalyticsContext = createContext(null);
362
+ /**
363
+ * Internal hook to access analytics context.
364
+ * Throws an error if used outside of an AnalyticsProvider.
365
+ *
366
+ * @internal
367
+ * @throws Error if used outside of AnalyticsProvider
368
+ */
369
+ const useAnalyticsContext = () => {
370
+ const context = useContext(AnalyticsContext);
371
+ if (!context) throw new Error("useAnalyticsContext must be used within an AnalyticsProvider");
372
+ return context;
373
+ };
374
+
375
+ //#endregion
376
+ //#region src/hooks/AnalyticsProvider.tsx
377
+ /**
378
+ * Analytics Provider component.
379
+ * Provides analytics functionality throughout the React component tree.
380
+ *
381
+ * @example
382
+ * ```tsx
383
+ * function App() {
384
+ * return (
385
+ * <AnalyticsProvider tagId={import.meta.env.VITE_GA_TAG_ID} autoInit={true}>
386
+ * <YourApp />
387
+ * </AnalyticsProvider>
388
+ * );
389
+ * }
390
+ * ```
391
+ */
392
+ const AnalyticsProvider = ({ tagId, autoInit = true, children }) => {
393
+ useEffect(() => {
394
+ if (autoInit && tagId) AnalyticsService.initialize(tagId);
395
+ }, [tagId, autoInit]);
396
+ const contextValue = useMemo(() => ({
397
+ tagId,
398
+ isEnabled: () => AnalyticsService.isEnabled(),
399
+ initialize: (tagIdOverride) => {
400
+ const effectiveTagId = tagIdOverride || tagId;
401
+ if (effectiveTagId) AnalyticsService.initialize(effectiveTagId);
402
+ },
403
+ trackEvent: (eventName, parameters) => {
404
+ try {
405
+ AnalyticsService.trackEvent(eventName, parameters);
406
+ } catch (error) {
407
+ logger.error("AnalyticsProvider", "Error tracking event:", error);
408
+ }
409
+ },
410
+ trackPageView: (pageName, pagePath) => {
411
+ try {
412
+ AnalyticsService.trackPageView(pageName, pagePath);
413
+ } catch (error) {
414
+ logger.error("AnalyticsProvider", "Error tracking page view:", error);
415
+ }
416
+ },
417
+ trackNetworkSelection: (networkId, ecosystem) => {
418
+ try {
419
+ AnalyticsService.trackNetworkSelection(networkId, ecosystem);
420
+ } catch (error) {
421
+ logger.error("AnalyticsProvider", "Error tracking network selection:", error);
422
+ }
423
+ }
424
+ }), [tagId]);
425
+ return /* @__PURE__ */ jsx(AnalyticsContext.Provider, {
426
+ value: contextValue,
427
+ children
428
+ });
429
+ };
430
+
431
+ //#endregion
432
+ //#region src/hooks/useAnalytics.ts
433
+ /**
434
+ * Custom hook for accessing analytics functionality.
435
+ *
436
+ * This hook provides a convenient interface for tracking user interactions
437
+ * throughout the application. It must be used within an AnalyticsProvider.
438
+ *
439
+ * For app-specific tracking methods, create a wrapper hook that uses this
440
+ * hook and adds your custom tracking functions.
441
+ *
442
+ * @example
443
+ * ```tsx
444
+ * // Basic usage
445
+ * function MyComponent() {
446
+ * const { trackEvent, trackPageView, isEnabled } = useAnalytics();
447
+ *
448
+ * const handleClick = () => {
449
+ * trackEvent('button_clicked', { button_name: 'submit' });
450
+ * };
451
+ *
452
+ * return (
453
+ * <div>
454
+ * Analytics enabled: {isEnabled().toString()}
455
+ * <button onClick={handleClick}>Submit</button>
456
+ * </div>
457
+ * );
458
+ * }
459
+ * ```
460
+ *
461
+ * @example
462
+ * ```tsx
463
+ * // Creating app-specific wrapper hook
464
+ * function useMyAppAnalytics() {
465
+ * const analytics = useAnalytics();
466
+ *
467
+ * return {
468
+ * ...analytics,
469
+ * trackFormSubmit: (formName: string) => {
470
+ * analytics.trackEvent('form_submitted', { form_name: formName });
471
+ * },
472
+ * };
473
+ * }
474
+ * ```
475
+ *
476
+ * @returns Analytics context with tracking methods and state
477
+ * @throws Error if used outside of AnalyticsProvider
478
+ */
479
+ const useAnalytics = () => {
480
+ try {
481
+ return useAnalyticsContext();
482
+ } catch {
483
+ throw new Error("useAnalytics must be used within an AnalyticsProvider");
484
+ }
485
+ };
486
+
487
+ //#endregion
488
+ //#region src/hooks/useDerivedAccountStatus.ts
489
+ const defaultAccountStatus = {
490
+ isConnected: false,
491
+ address: void 0,
492
+ chainId: void 0
493
+ };
494
+ /**
495
+ * A custom hook that consumes useWalletState to get the walletFacadeHooks,
496
+ * then calls the useAccount facade hook (if available) and returns a structured,
497
+ * safely-accessed account status (isConnected, address, chainId).
498
+ * Provides default values if the hook or its properties are unavailable.
499
+ */
500
+ function useDerivedAccountStatus() {
501
+ const { walletFacadeHooks } = useWalletState();
502
+ const accountHookOutput = walletFacadeHooks?.useAccount ? walletFacadeHooks.useAccount() : void 0;
503
+ if (isRecordWithProperties(accountHookOutput)) return {
504
+ isConnected: "isConnected" in accountHookOutput && typeof accountHookOutput.isConnected === "boolean" ? accountHookOutput.isConnected : defaultAccountStatus.isConnected,
505
+ address: "address" in accountHookOutput && typeof accountHookOutput.address === "string" ? accountHookOutput.address : defaultAccountStatus.address,
506
+ chainId: "chainId" in accountHookOutput && typeof accountHookOutput.chainId === "number" ? accountHookOutput.chainId : defaultAccountStatus.chainId
507
+ };
508
+ return defaultAccountStatus;
509
+ }
510
+
511
+ //#endregion
512
+ //#region src/hooks/useDerivedSwitchChainStatus.ts
513
+ const defaultSwitchChainStatus = {
514
+ switchChain: void 0,
515
+ isSwitching: false,
516
+ error: null
517
+ };
518
+ /**
519
+ * A custom hook that consumes `useWalletState` to get `walletFacadeHooks`,
520
+ * then calls the `useSwitchChain` facade hook (if available) and returns a structured,
521
+ * safely-accessed status and control function for network switching.
522
+ * Provides default values if the hook or its properties are unavailable.
523
+ */
524
+ function useDerivedSwitchChainStatus() {
525
+ const { walletFacadeHooks } = useWalletState();
526
+ const switchChainHookOutput = walletFacadeHooks?.useSwitchChain ? walletFacadeHooks.useSwitchChain() : void 0;
527
+ if (isRecordWithProperties(switchChainHookOutput)) return {
528
+ switchChain: "switchChain" in switchChainHookOutput && typeof switchChainHookOutput.switchChain === "function" ? switchChainHookOutput.switchChain : defaultSwitchChainStatus.switchChain,
529
+ isSwitching: "isPending" in switchChainHookOutput && typeof switchChainHookOutput.isPending === "boolean" ? switchChainHookOutput.isPending : defaultSwitchChainStatus.isSwitching,
530
+ error: "error" in switchChainHookOutput && switchChainHookOutput.error instanceof Error ? switchChainHookOutput.error : defaultSwitchChainStatus.error
531
+ };
532
+ return defaultSwitchChainStatus;
533
+ }
534
+
535
+ //#endregion
536
+ //#region src/hooks/useDerivedChainInfo.ts
537
+ const defaultChainInfo = {
538
+ currentChainId: void 0,
539
+ availableChains: []
540
+ };
541
+ /**
542
+ * A custom hook that consumes `useWalletState` to get `walletFacadeHooks`,
543
+ * then calls the `useChainId` and `useChains` facade hooks (if available)
544
+ * and returns a structured object with this information.
545
+ * Provides default values if the hooks or their properties are unavailable.
546
+ */
547
+ function useDerivedChainInfo() {
548
+ const { walletFacadeHooks } = useWalletState();
549
+ let chainIdToReturn = defaultChainInfo.currentChainId;
550
+ const chainIdHookOutput = walletFacadeHooks?.useChainId ? walletFacadeHooks.useChainId() : void 0;
551
+ if (typeof chainIdHookOutput === "number") chainIdToReturn = chainIdHookOutput;
552
+ else if (chainIdHookOutput !== void 0) logger.warn("useDerivedChainInfo", "useChainId facade hook returned non-numeric value:", chainIdHookOutput);
553
+ let chainsToReturn = defaultChainInfo.availableChains;
554
+ const chainsHookOutput = walletFacadeHooks?.useChains ? walletFacadeHooks.useChains() : void 0;
555
+ if (Array.isArray(chainsHookOutput)) chainsToReturn = chainsHookOutput;
556
+ else if (chainsHookOutput !== void 0) logger.warn("useDerivedChainInfo", "useChains facade hook returned non-array value:", chainsHookOutput);
557
+ return {
558
+ currentChainId: chainIdToReturn,
559
+ availableChains: chainsToReturn
560
+ };
561
+ }
562
+
563
+ //#endregion
564
+ //#region src/hooks/useDerivedConnectStatus.ts
565
+ const defaultConnectStatus = {
566
+ connect: void 0,
567
+ connectors: [],
568
+ isConnecting: false,
569
+ error: null,
570
+ pendingConnector: void 0
571
+ };
572
+ /**
573
+ * A custom hook that consumes `useWalletState` to get `walletFacadeHooks`,
574
+ * then calls the `useConnect` facade hook (if available) and returns a structured,
575
+ * safely-accessed status and control functions for wallet connection.
576
+ */
577
+ function useDerivedConnectStatus() {
578
+ const { walletFacadeHooks } = useWalletState();
579
+ const connectHookOutput = walletFacadeHooks?.useConnect ? walletFacadeHooks.useConnect() : void 0;
580
+ if (isRecordWithProperties(connectHookOutput)) return {
581
+ connect: "connect" in connectHookOutput && typeof connectHookOutput.connect === "function" ? connectHookOutput.connect : defaultConnectStatus.connect,
582
+ connectors: "connectors" in connectHookOutput && Array.isArray(connectHookOutput.connectors) ? connectHookOutput.connectors : defaultConnectStatus.connectors,
583
+ isConnecting: "isPending" in connectHookOutput && typeof connectHookOutput.isPending === "boolean" ? connectHookOutput.isPending : "isLoading" in connectHookOutput && typeof connectHookOutput.isLoading === "boolean" ? connectHookOutput.isLoading : defaultConnectStatus.isConnecting,
584
+ error: "error" in connectHookOutput && connectHookOutput.error instanceof Error ? connectHookOutput.error : defaultConnectStatus.error,
585
+ pendingConnector: "pendingConnector" in connectHookOutput && typeof connectHookOutput.pendingConnector === "object" ? connectHookOutput.pendingConnector : defaultConnectStatus.pendingConnector
586
+ };
587
+ return defaultConnectStatus;
588
+ }
589
+
590
+ //#endregion
591
+ //#region src/hooks/useDerivedDisconnect.ts
592
+ const defaultDisconnectStatus = {
593
+ disconnect: void 0,
594
+ isDisconnecting: false,
595
+ error: null
596
+ };
597
+ /**
598
+ * A custom hook that consumes `useWalletState` to get `walletFacadeHooks`,
599
+ * then calls the `useDisconnect` facade hook (if available) and returns a structured,
600
+ * safely-accessed status and control function for wallet disconnection.
601
+ */
602
+ function useDerivedDisconnect() {
603
+ const { walletFacadeHooks } = useWalletState();
604
+ const disconnectHookOutput = walletFacadeHooks?.useDisconnect ? walletFacadeHooks.useDisconnect() : void 0;
605
+ if (isRecordWithProperties(disconnectHookOutput)) return {
606
+ disconnect: "disconnect" in disconnectHookOutput && typeof disconnectHookOutput.disconnect === "function" ? disconnectHookOutput.disconnect : defaultDisconnectStatus.disconnect,
607
+ isDisconnecting: "isPending" in disconnectHookOutput && typeof disconnectHookOutput.isPending === "boolean" ? disconnectHookOutput.isPending : "isLoading" in disconnectHookOutput && typeof disconnectHookOutput.isLoading === "boolean" ? disconnectHookOutput.isLoading : defaultDisconnectStatus.isDisconnecting,
608
+ error: "error" in disconnectHookOutput && disconnectHookOutput.error instanceof Error ? disconnectHookOutput.error : defaultDisconnectStatus.error
609
+ };
610
+ return defaultDisconnectStatus;
611
+ }
612
+
613
+ //#endregion
614
+ //#region src/hooks/useWalletReconnectionHandler.ts
615
+ /**
616
+ * Hook that detects wallet reconnection and re-queues network switch if needed.
617
+ *
618
+ * When a user disconnects their wallet and then reconnects in the same session,
619
+ * the wallet may connect to a different chain than what's selected in the app.
620
+ * This hook detects that scenario and invokes a callback to re-queue the network switch.
621
+ *
622
+ * @param selectedNetworkConfigId - Currently selected network in the app
623
+ * @param selectedAdapter - Currently active adapter instance
624
+ * @param networkToSwitchTo - Current network switch queue state (null if no switch pending)
625
+ * @param onRequeueSwitch - Callback invoked when a network switch should be re-queued
626
+ */
627
+ function useWalletReconnectionHandler(selectedNetworkConfigId, selectedAdapter, networkToSwitchTo, onRequeueSwitch) {
628
+ const { isConnected, chainId: walletChainId } = useDerivedAccountStatus();
629
+ const prevConnectedRef = useRef(isConnected);
630
+ useEffect(() => {
631
+ const isReconnection = !prevConnectedRef.current && isConnected;
632
+ prevConnectedRef.current = isConnected;
633
+ if (!isReconnection || !selectedNetworkConfigId || !selectedAdapter) return;
634
+ if (networkToSwitchTo === selectedNetworkConfigId) return;
635
+ const adapterConfig = selectedAdapter.networkConfig;
636
+ if (!("chainId" in adapterConfig) || !walletChainId) return;
637
+ const targetChainId = Number(adapterConfig.chainId);
638
+ if (walletChainId !== targetChainId) {
639
+ logger.info("useWalletReconnectionHandler", `Wallet reconnected with chain ${walletChainId}, but selected network requires ${targetChainId}. Re-queueing switch.`);
640
+ onRequeueSwitch(selectedNetworkConfigId);
641
+ }
642
+ }, [
643
+ isConnected,
644
+ walletChainId,
645
+ selectedNetworkConfigId,
646
+ selectedAdapter,
647
+ networkToSwitchTo,
648
+ onRequeueSwitch
649
+ ]);
650
+ }
651
+
652
+ //#endregion
653
+ //#region src/components/WalletConnectionUI.tsx
654
+ /**
655
+ * Component that displays wallet connection UI components
656
+ * provided by the active adapter.
657
+ */
658
+ const WalletConnectionUI = ({ className }) => {
659
+ const [isError, setIsError] = useState(false);
660
+ const { activeAdapter, walletFacadeHooks } = useWalletState();
661
+ useEffect(() => {
662
+ logger.debug("WalletConnectionUI", "[Debug] State from useWalletState:", {
663
+ adapterId: activeAdapter?.networkConfig.id,
664
+ hasFacadeHooks: !!walletFacadeHooks
665
+ });
666
+ }, [activeAdapter, walletFacadeHooks]);
667
+ const walletComponents = (() => {
668
+ if (!activeAdapter || typeof activeAdapter.getEcosystemWalletComponents !== "function") {
669
+ logger.debug("WalletConnectionUI", "[Debug] No activeAdapter or getEcosystemWalletComponents method, returning null.");
670
+ return null;
671
+ }
672
+ try {
673
+ const components = activeAdapter.getEcosystemWalletComponents();
674
+ logger.debug("WalletConnectionUI", "[Debug] walletComponents from adapter:", components);
675
+ return components;
676
+ } catch (error) {
677
+ logger.error("WalletConnectionUI", "[Debug] Error getting wallet components:", error);
678
+ setIsError(true);
679
+ return null;
680
+ }
681
+ })();
682
+ if (!walletComponents) {
683
+ logger.debug("WalletConnectionUI", "[Debug] getEcosystemWalletComponents returned null/undefined, rendering null.");
684
+ return null;
685
+ }
686
+ logger.debug("WalletConnectionUI", "Rendering wallet components:", {
687
+ hasConnectButton: !!walletComponents.ConnectButton,
688
+ hasAccountDisplay: !!walletComponents.AccountDisplay,
689
+ hasNetworkSwitcher: !!walletComponents.NetworkSwitcher
690
+ });
691
+ const { ConnectButton, AccountDisplay, NetworkSwitcher } = walletComponents;
692
+ if (isError) return /* @__PURE__ */ jsx("div", {
693
+ className: cn("flex items-center gap-4", className),
694
+ children: /* @__PURE__ */ jsx(Button, {
695
+ variant: "destructive",
696
+ size: "sm",
697
+ onClick: () => window.location.reload(),
698
+ children: "Wallet Error - Retry"
699
+ })
700
+ });
701
+ return /* @__PURE__ */ jsxs("div", {
702
+ className: cn("flex items-center gap-4", className),
703
+ children: [
704
+ NetworkSwitcher && /* @__PURE__ */ jsx(NetworkSwitcher, {}),
705
+ AccountDisplay && /* @__PURE__ */ jsx(AccountDisplay, {}),
706
+ ConnectButton && /* @__PURE__ */ jsx(ConnectButton, {})
707
+ ]
708
+ });
709
+ };
710
+
711
+ //#endregion
712
+ //#region src/components/WalletConnectionHeader.tsx
713
+ /**
714
+ * Component that renders the wallet connection UI.
715
+ * Uses useWalletState to get its data.
716
+ */
717
+ const WalletConnectionHeader = () => {
718
+ const { isAdapterLoading, activeAdapter } = useWalletState();
719
+ useEffect(() => {
720
+ logger.debug("WalletConnectionHeader", "[Debug] State from useWalletState:", {
721
+ adapterPresent: !!activeAdapter,
722
+ adapterNetwork: activeAdapter?.networkConfig.id,
723
+ isLoading: isAdapterLoading
724
+ });
725
+ }, [activeAdapter, isAdapterLoading]);
726
+ if (isAdapterLoading) {
727
+ logger.debug("WalletConnectionHeader", "[Debug] Adapter loading, showing skeleton.");
728
+ return /* @__PURE__ */ jsx("div", { className: "h-9 w-28 animate-pulse rounded bg-muted" });
729
+ }
730
+ return /* @__PURE__ */ jsx(WalletConnectionUI, {});
731
+ };
732
+
733
+ //#endregion
734
+ //#region src/components/NetworkSwitchManager.tsx
735
+ /**
736
+ * Component that handles wallet network switching based on the selected network.
737
+ *
738
+ * This component manages the lifecycle of network switching operations,
739
+ * coordinating between the wallet's current chain state and the target network.
740
+ * It's designed to be used in any application that needs seamless wallet network switching.
741
+ *
742
+ * Features:
743
+ * - Automatically initiates network switch when mounted with a target network
744
+ * - Handles EVM chain switching gracefully
745
+ * - No-ops for non-EVM networks that don't support chain switching
746
+ * - Tracks switch attempts to prevent duplicate operations
747
+ * - Provides completion callback for parent components to handle state cleanup
748
+ */
749
+ const NetworkSwitchManager = ({ adapter, targetNetworkId, onNetworkSwitchComplete }) => {
750
+ const isMountedRef = useRef(true);
751
+ const [hasAttemptedSwitch, setHasAttemptedSwitch] = useState(false);
752
+ const { isConnected, chainId: currentChainIdFromHook } = useDerivedAccountStatus();
753
+ const { switchChain: execSwitchNetwork, isSwitching: isSwitchingNetworkViaHook, error: switchNetworkError } = useDerivedSwitchChainStatus();
754
+ useEffect(() => {
755
+ isMountedRef.current = true;
756
+ logger.info("NetworkSwitchManager", `Mounted with target: ${targetNetworkId}, current attempt status: ${hasAttemptedSwitch}`);
757
+ setHasAttemptedSwitch(false);
758
+ return () => {
759
+ logger.info("NetworkSwitchManager", `Unmounting, was for target: ${targetNetworkId}`);
760
+ isMountedRef.current = false;
761
+ };
762
+ }, [targetNetworkId]);
763
+ useEffect(() => {
764
+ logger.info("NetworkSwitchManager", "State Update:", {
765
+ target: targetNetworkId,
766
+ adapterNetwork: adapter.networkConfig.id,
767
+ isSwitching: isSwitchingNetworkViaHook,
768
+ hookError: !!switchNetworkError,
769
+ canExec: !!execSwitchNetwork,
770
+ connected: isConnected,
771
+ walletChain: currentChainIdFromHook,
772
+ attempted: hasAttemptedSwitch
773
+ });
774
+ }, [
775
+ adapter,
776
+ targetNetworkId,
777
+ isSwitchingNetworkViaHook,
778
+ switchNetworkError,
779
+ execSwitchNetwork,
780
+ isConnected,
781
+ currentChainIdFromHook,
782
+ hasAttemptedSwitch
783
+ ]);
784
+ useEffect(() => {
785
+ const completeOperation = (logMessage, options = { notifyComplete: true }) => {
786
+ if (logMessage) logger.info("NetworkSwitchManager", logMessage);
787
+ if (options.notifyComplete && isMountedRef.current && onNetworkSwitchComplete) onNetworkSwitchComplete();
788
+ if (isMountedRef.current) setHasAttemptedSwitch(false);
789
+ };
790
+ if (!execSwitchNetwork) {
791
+ completeOperation("No switchChain function available from hook. Operation halted.", { notifyComplete: false });
792
+ return;
793
+ }
794
+ if (isSwitchingNetworkViaHook && hasAttemptedSwitch) {
795
+ logger.info("NetworkSwitchManager", "Hook reports switch in progress for current attempt. Waiting...");
796
+ return;
797
+ }
798
+ if (hasAttemptedSwitch && !isSwitchingNetworkViaHook) {
799
+ logger.info("NetworkSwitchManager", "Previous switch attempt concluded. Deferring to completion effect.");
800
+ return;
801
+ }
802
+ if (adapter.networkConfig.id !== targetNetworkId) {
803
+ completeOperation(`CRITICAL: Adapter (${adapter.networkConfig.id}) vs Target (${targetNetworkId}) mismatch. Operation halted.`, { notifyComplete: false });
804
+ return;
805
+ }
806
+ if (!isConnected) {
807
+ completeOperation("Wallet not connected (derived status). Awaiting connection.", { notifyComplete: false });
808
+ return;
809
+ }
810
+ if (!("chainId" in adapter.networkConfig)) {
811
+ completeOperation("Network does not support chain switching (non-EVM). Operation complete (no-op).");
812
+ return;
813
+ }
814
+ const targetChainToBeSwitchedTo = Number(adapter.networkConfig.chainId);
815
+ if (currentChainIdFromHook === targetChainToBeSwitchedTo) {
816
+ completeOperation("Already on correct chain (derived status). Operation complete.");
817
+ return;
818
+ }
819
+ const performSwitchActual = () => {
820
+ if (!isMountedRef.current || isSwitchingNetworkViaHook || hasAttemptedSwitch) {
821
+ logger.info("NetworkSwitchManager", `Switch attempt aborted in timeout or already handled. Conditions: isSwitching: ${isSwitchingNetworkViaHook}, hasAttempted: ${hasAttemptedSwitch}`);
822
+ return;
823
+ }
824
+ logger.info("NetworkSwitchManager", `Attempting switch to ${targetChainToBeSwitchedTo} via derived hook.`);
825
+ setHasAttemptedSwitch(true);
826
+ execSwitchNetwork({ chainId: targetChainToBeSwitchedTo });
827
+ };
828
+ const timeoutId = setTimeout(performSwitchActual, 100);
829
+ return () => clearTimeout(timeoutId);
830
+ }, [
831
+ adapter,
832
+ targetNetworkId,
833
+ execSwitchNetwork,
834
+ isSwitchingNetworkViaHook,
835
+ onNetworkSwitchComplete,
836
+ isConnected,
837
+ currentChainIdFromHook,
838
+ hasAttemptedSwitch
839
+ ]);
840
+ useEffect(() => {
841
+ if (!isMountedRef.current || !execSwitchNetwork || !hasAttemptedSwitch) return;
842
+ if (!isSwitchingNetworkViaHook) {
843
+ let completionMessage = "Switch hook operation concluded.";
844
+ if (switchNetworkError) {
845
+ logger.error("NetworkSwitchManager", "Error from derived switch hook:", switchNetworkError);
846
+ completionMessage = "Switch hook completed with error.";
847
+ } else logger.info("NetworkSwitchManager", "Derived switch hook completed successfully.");
848
+ if (onNetworkSwitchComplete) onNetworkSwitchComplete();
849
+ if (isMountedRef.current) setHasAttemptedSwitch(false);
850
+ logger.info("NetworkSwitchManager", completionMessage);
851
+ }
852
+ }, [
853
+ isSwitchingNetworkViaHook,
854
+ switchNetworkError,
855
+ execSwitchNetwork,
856
+ hasAttemptedSwitch,
857
+ onNetworkSwitchComplete
858
+ ]);
859
+ return null;
860
+ };
861
+
862
+ //#endregion
863
+ export { AdapterContext, AdapterProvider, AnalyticsContext, AnalyticsProvider, NetworkSwitchManager, WalletConnectionHeader, WalletConnectionUI, WalletStateContext, WalletStateProvider, useAdapterContext, useAnalytics, useDerivedAccountStatus, useDerivedChainInfo, useDerivedConnectStatus, useDerivedDisconnect, useDerivedSwitchChainStatus, useWalletReconnectionHandler, useWalletState };
864
+ //# sourceMappingURL=index.js.map