UNPKG

@openzeppelin/contracts-ui-builder-react-core

Version:

Core React context providers and hooks for the OpenZeppelin Contracts UI Builder.

586 lines (572 loc) 23.5 kB
// src/hooks/AdapterContext.tsx import { createContext } from "react"; var AdapterContext = createContext(null); // src/hooks/AdapterProvider.tsx import { useCallback, useEffect, useMemo, useState } from "react"; import { logger } from "@openzeppelin/contracts-ui-builder-utils"; import { jsx } from "react/jsx-runtime"; function AdapterProvider({ children, resolveAdapter }) { const [adapterRegistry, setAdapterRegistry] = useState({}); const [loadingNetworks, setLoadingNetworks] = useState(/* @__PURE__ */ new Set()); useEffect(() => { const adapterCount = Object.keys(adapterRegistry).length; if (adapterCount > 0) { logger.info("AdapterProvider", `Registry contains ${adapterCount} adapters:`, { networkIds: Object.keys(adapterRegistry), loadingCount: loadingNetworks.size, loadingNetworkIds: Array.from(loadingNetworks) }); } }, [adapterRegistry, loadingNetworks]); const getAdapterForNetwork = useCallback( (networkConfig) => { if (!networkConfig) { return { adapter: null, isLoading: false }; } const networkId = networkConfig.id; logger.debug("AdapterProvider", `Adapter requested for network ${networkId}`); if (adapterRegistry[networkId]) { logger.debug("AdapterProvider", `Using existing adapter for network ${networkId}`); return { adapter: adapterRegistry[networkId], isLoading: false }; } if (loadingNetworks.has(networkId)) { logger.debug("AdapterProvider", `Adapter for network ${networkId} is currently loading`); return { adapter: null, isLoading: true }; } setLoadingNetworks((prev) => { const newSet = new Set(prev); newSet.add(networkId); return newSet; }); logger.info( "AdapterProvider", `Starting adapter initialization for network ${networkId} (${networkConfig.name})` ); void resolveAdapter(networkConfig).then((adapter) => { logger.info("AdapterProvider", `Adapter for network ${networkId} loaded successfully`, { type: adapter.constructor.name, objectId: Object.prototype.toString.call(adapter) }); setAdapterRegistry((prev) => ({ ...prev, [networkId]: adapter })); setLoadingNetworks((prev) => { const newSet = new Set(prev); newSet.delete(networkId); return newSet; }); }).catch((error) => { logger.error("AdapterProvider", `Error loading adapter for network ${networkId}:`, error); setLoadingNetworks((prev) => { const newSet = new Set(prev); newSet.delete(networkId); return newSet; }); }); return { adapter: null, isLoading: true }; }, [adapterRegistry, loadingNetworks, resolveAdapter] ); const contextValue = useMemo( () => ({ getAdapterForNetwork }), [getAdapterForNetwork] ); return /* @__PURE__ */ jsx(AdapterContext.Provider, { value: contextValue, children }); } // src/hooks/WalletStateContext.tsx import React, { createContext as createContext2 } from "react"; var WalletStateContext = createContext2(void 0); function useWalletState() { const context = React.useContext(WalletStateContext); if (context === void 0) { throw new Error("useWalletState must be used within a WalletStateProvider"); } return context; } // src/hooks/WalletStateProvider.tsx import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useState as useState2 } from "react"; import { logger as logger2 } from "@openzeppelin/contracts-ui-builder-utils"; // src/hooks/useAdapterContext.ts import { useContext } from "react"; function useAdapterContext() { const context = useContext(AdapterContext); if (!context) { throw new Error("useAdapterContext must be used within an AdapterProvider"); } return context; } // src/hooks/WalletStateProvider.tsx import { jsx as jsx2 } from "react/jsx-runtime"; async function configureAdapterUiKit(adapter, loadConfigModule, programmaticOverrides = {}) { try { if (typeof adapter.configureUiKit === "function") { logger2.info( "[WSP configureAdapterUiKit] Calling configureUiKit for adapter:", adapter?.networkConfig?.id ); await adapter.configureUiKit(programmaticOverrides, { loadUiKitNativeConfig: loadConfigModule }); logger2.info( "[WSP configureAdapterUiKit] configureUiKit completed for adapter:", adapter?.networkConfig?.id ); } const providerComponent = adapter.getEcosystemReactUiContextProvider?.() || null; const hooks = adapter.getEcosystemReactHooks?.() || null; logger2.info("[WSP configureAdapterUiKit]", "UI provider and hooks retrieved successfully."); return { providerComponent, hooks }; } catch (error) { logger2.error("[WSP configureAdapterUiKit]", "Error during adapter UI setup:", error); throw error; } } function WalletStateProvider({ children, initialNetworkId = null, getNetworkConfigById, loadConfigModule }) { const [currentGlobalNetworkId, setCurrentGlobalNetworkIdState] = useState2( initialNetworkId ); const [currentGlobalNetworkConfig, setCurrentGlobalNetworkConfig] = useState2(null); const [globalActiveAdapter, setGlobalActiveAdapter] = useState2(null); const [isGlobalAdapterLoading, setIsGlobalAdapterLoading] = useState2(false); const [walletFacadeHooks, setWalletFacadeHooks] = useState2( null ); const [AdapterUiContextProviderToRender, setAdapterUiContextProviderToRender] = useState2(null); const [uiKitConfigVersion, setUiKitConfigVersion] = useState2(0); const [programmaticUiKitConfig, setProgrammaticUiKitConfig] = useState2(void 0); const { getAdapterForNetwork } = useAdapterContext(); useEffect2(() => { const abortController = new AbortController(); async function fetchNetworkConfig() { if (!currentGlobalNetworkId) { if (!abortController.signal.aborted) { setCurrentGlobalNetworkConfig(null); } return; } try { const config = await Promise.resolve(getNetworkConfigById(currentGlobalNetworkId)); if (!abortController.signal.aborted) { setCurrentGlobalNetworkConfig(config || null); } } catch (error) { if (!abortController.signal.aborted) { logger2.error("[WSP fetchNetworkConfig]", "Failed to fetch network config:", error); setCurrentGlobalNetworkConfig(null); } } } void fetchNetworkConfig(); return () => abortController.abort(); }, [currentGlobalNetworkId, getNetworkConfigById]); useEffect2(() => { const abortController = new AbortController(); async function loadAdapterAndConfigureUi() { if (!currentGlobalNetworkConfig) { if (!abortController.signal.aborted) { setGlobalActiveAdapter(null); setIsGlobalAdapterLoading(false); setAdapterUiContextProviderToRender(null); setWalletFacadeHooks(null); } return; } const { adapter: newAdapter, isLoading: newIsLoading } = getAdapterForNetwork( currentGlobalNetworkConfig ); if (abortController.signal.aborted) return; setGlobalActiveAdapter(newAdapter); setIsGlobalAdapterLoading(newIsLoading); if (newAdapter && !newIsLoading) { try { const { providerComponent, hooks } = await configureAdapterUiKit( newAdapter, loadConfigModule, programmaticUiKitConfig ); if (!abortController.signal.aborted) { setAdapterUiContextProviderToRender(() => providerComponent); setWalletFacadeHooks(hooks); } } catch (error) { if (!abortController.signal.aborted) { logger2.error( "[WSP loadAdapterAndConfigureUi]", "Error during adapter UI setup:", error ); setAdapterUiContextProviderToRender(null); setWalletFacadeHooks(null); } } } else if (!newAdapter && !newIsLoading) { if (!abortController.signal.aborted) { setAdapterUiContextProviderToRender(null); setWalletFacadeHooks(null); } } } void loadAdapterAndConfigureUi(); return () => abortController.abort(); }, [ currentGlobalNetworkConfig, getAdapterForNetwork, loadConfigModule, uiKitConfigVersion, programmaticUiKitConfig ]); const setActiveNetworkIdCallback = useCallback2((networkId) => { logger2.info("WalletStateProvider", `Setting global network ID to: ${networkId}`); setCurrentGlobalNetworkIdState(networkId); if (!networkId) { setCurrentGlobalNetworkConfig(null); setGlobalActiveAdapter(null); setIsGlobalAdapterLoading(false); setWalletFacadeHooks(null); } }, []); const reconfigureActiveAdapterUiKit = useCallback2( (uiKitConfig) => { logger2.info( "WalletStateProvider", "Explicitly triggering UI kit re-configuration by bumping version.", uiKitConfig ); setProgrammaticUiKitConfig(uiKitConfig); setUiKitConfigVersion((v) => v + 1); }, [setProgrammaticUiKitConfig, setUiKitConfigVersion] ); const contextValue = useMemo2( () => ({ activeNetworkId: currentGlobalNetworkId, setActiveNetworkId: setActiveNetworkIdCallback, activeNetworkConfig: currentGlobalNetworkConfig, activeAdapter: globalActiveAdapter, isAdapterLoading: isGlobalAdapterLoading, walletFacadeHooks, reconfigureActiveAdapterUiKit }), [ currentGlobalNetworkId, setActiveNetworkIdCallback, currentGlobalNetworkConfig, globalActiveAdapter, isGlobalAdapterLoading, walletFacadeHooks, reconfigureActiveAdapterUiKit ] ); const ActualProviderToRender = AdapterUiContextProviderToRender; let childrenToRender; if (ActualProviderToRender) { const key = globalActiveAdapter?.lastFullUiKitConfiguration?.kitName; logger2.info( "[WSP RENDER]", "Rendering adapter-provided UI context provider:", ActualProviderToRender.displayName || ActualProviderToRender.name || "UnknownComponent" ); childrenToRender = /* @__PURE__ */ jsx2(ActualProviderToRender, { children }, key); } else { logger2.info( "[WSP RENDER]", "No adapter UI context provider to render. Rendering direct children." ); childrenToRender = children; } return /* @__PURE__ */ jsx2(WalletStateContext.Provider, { value: contextValue, children: childrenToRender }); } // src/hooks/useDerivedAccountStatus.ts import { isRecordWithProperties } from "@openzeppelin/contracts-ui-builder-utils"; var defaultAccountStatus = { isConnected: false, address: void 0, chainId: void 0 }; function useDerivedAccountStatus() { const { walletFacadeHooks } = useWalletState(); const accountHookOutput = walletFacadeHooks?.useAccount ? walletFacadeHooks.useAccount() : void 0; if (isRecordWithProperties(accountHookOutput)) { const isConnected = "isConnected" in accountHookOutput && typeof accountHookOutput.isConnected === "boolean" ? accountHookOutput.isConnected : defaultAccountStatus.isConnected; const address = "address" in accountHookOutput && typeof accountHookOutput.address === "string" ? accountHookOutput.address : defaultAccountStatus.address; const chainId = "chainId" in accountHookOutput && typeof accountHookOutput.chainId === "number" ? accountHookOutput.chainId : defaultAccountStatus.chainId; return { isConnected, address, chainId }; } return defaultAccountStatus; } // src/hooks/useDerivedSwitchChainStatus.ts import { isRecordWithProperties as isRecordWithProperties2 } from "@openzeppelin/contracts-ui-builder-utils"; var defaultSwitchChainStatus = { switchChain: void 0, isSwitching: false, error: null }; function useDerivedSwitchChainStatus() { const { walletFacadeHooks } = useWalletState(); const switchChainHookOutput = walletFacadeHooks?.useSwitchChain ? walletFacadeHooks.useSwitchChain() : void 0; if (isRecordWithProperties2(switchChainHookOutput)) { const execSwitchFn = "switchChain" in switchChainHookOutput && typeof switchChainHookOutput.switchChain === "function" ? switchChainHookOutput.switchChain : defaultSwitchChainStatus.switchChain; const isPending = "isPending" in switchChainHookOutput && typeof switchChainHookOutput.isPending === "boolean" ? switchChainHookOutput.isPending : defaultSwitchChainStatus.isSwitching; const err = "error" in switchChainHookOutput && switchChainHookOutput.error instanceof Error ? switchChainHookOutput.error : defaultSwitchChainStatus.error; return { switchChain: execSwitchFn, isSwitching: isPending, error: err }; } return defaultSwitchChainStatus; } // src/hooks/useDerivedChainInfo.ts import { logger as logger3 } from "@openzeppelin/contracts-ui-builder-utils"; var defaultChainInfo = { currentChainId: void 0, availableChains: [] }; function useDerivedChainInfo() { const { walletFacadeHooks } = useWalletState(); let chainIdToReturn = defaultChainInfo.currentChainId; const chainIdHookOutput = walletFacadeHooks?.useChainId ? walletFacadeHooks.useChainId() : void 0; if (typeof chainIdHookOutput === "number") { chainIdToReturn = chainIdHookOutput; } else if (chainIdHookOutput !== void 0) { logger3.warn( "useDerivedChainInfo", "useChainId facade hook returned non-numeric value:", chainIdHookOutput ); } let chainsToReturn = defaultChainInfo.availableChains; const chainsHookOutput = walletFacadeHooks?.useChains ? walletFacadeHooks.useChains() : void 0; if (Array.isArray(chainsHookOutput)) { chainsToReturn = chainsHookOutput; } else if (chainsHookOutput !== void 0) { logger3.warn( "useDerivedChainInfo", "useChains facade hook returned non-array value:", chainsHookOutput ); } return { currentChainId: chainIdToReturn, availableChains: chainsToReturn }; } // src/hooks/useDerivedConnectStatus.ts import { isRecordWithProperties as isRecordWithProperties3 } from "@openzeppelin/contracts-ui-builder-utils"; var defaultConnectStatus = { connect: void 0, connectors: [], isConnecting: false, error: null, pendingConnector: void 0 }; function useDerivedConnectStatus() { const { walletFacadeHooks } = useWalletState(); const connectHookOutput = walletFacadeHooks?.useConnect ? walletFacadeHooks.useConnect() : void 0; if (isRecordWithProperties3(connectHookOutput)) { const connectFn = "connect" in connectHookOutput && typeof connectHookOutput.connect === "function" ? connectHookOutput.connect : defaultConnectStatus.connect; const conns = "connectors" in connectHookOutput && Array.isArray(connectHookOutput.connectors) ? connectHookOutput.connectors : defaultConnectStatus.connectors; const isPending = "isPending" in connectHookOutput && typeof connectHookOutput.isPending === "boolean" ? connectHookOutput.isPending : "isLoading" in connectHookOutput && typeof connectHookOutput.isLoading === "boolean" ? connectHookOutput.isLoading : defaultConnectStatus.isConnecting; const err = "error" in connectHookOutput && connectHookOutput.error instanceof Error ? connectHookOutput.error : defaultConnectStatus.error; const pendingConn = "pendingConnector" in connectHookOutput && typeof connectHookOutput.pendingConnector === "object" ? connectHookOutput.pendingConnector : defaultConnectStatus.pendingConnector; return { connect: connectFn, connectors: conns, isConnecting: isPending, error: err, pendingConnector: pendingConn }; } return defaultConnectStatus; } // src/hooks/useDerivedDisconnect.ts import { isRecordWithProperties as isRecordWithProperties4 } from "@openzeppelin/contracts-ui-builder-utils"; var defaultDisconnectStatus = { disconnect: void 0, isDisconnecting: false, error: null }; function useDerivedDisconnect() { const { walletFacadeHooks } = useWalletState(); const disconnectHookOutput = walletFacadeHooks?.useDisconnect ? walletFacadeHooks.useDisconnect() : void 0; if (isRecordWithProperties4(disconnectHookOutput)) { const disconnectFn = "disconnect" in disconnectHookOutput && typeof disconnectHookOutput.disconnect === "function" ? disconnectHookOutput.disconnect : defaultDisconnectStatus.disconnect; const isPending = "isPending" in disconnectHookOutput && typeof disconnectHookOutput.isPending === "boolean" ? disconnectHookOutput.isPending : "isLoading" in disconnectHookOutput && typeof disconnectHookOutput.isLoading === "boolean" ? disconnectHookOutput.isLoading : defaultDisconnectStatus.isDisconnecting; const err = "error" in disconnectHookOutput && disconnectHookOutput.error instanceof Error ? disconnectHookOutput.error : defaultDisconnectStatus.error; return { disconnect: disconnectFn, isDisconnecting: isPending, error: err }; } return defaultDisconnectStatus; } // src/components/WalletConnectionHeader.tsx import { useEffect as useEffect4 } from "react"; import { logger as logger5 } from "@openzeppelin/contracts-ui-builder-utils"; // src/components/WalletConnectionUI.tsx import { useEffect as useEffect3, useMemo as useMemo3, useState as useState3 } from "react"; import { Button } from "@openzeppelin/contracts-ui-builder-ui"; import { cn, logger as logger4 } from "@openzeppelin/contracts-ui-builder-utils"; import { jsx as jsx3, jsxs } from "react/jsx-runtime"; var WalletConnectionUI = ({ className }) => { const [isError, setIsError] = useState3(false); const { activeAdapter, walletFacadeHooks } = useWalletState(); useEffect3(() => { const handleError = () => { setIsError(true); }; window.addEventListener("error", handleError); return () => { window.removeEventListener("error", handleError); }; }, []); useEffect3(() => { logger4.debug("WalletConnectionUI", "[Debug] State from useWalletState:", { adapterId: activeAdapter?.networkConfig.id, hasFacadeHooks: !!walletFacadeHooks }); }, [activeAdapter, walletFacadeHooks]); const walletComponents = useMemo3(() => { if (!activeAdapter || typeof activeAdapter.getEcosystemWalletComponents !== "function") { logger4.debug( "WalletConnectionUI", "[Debug] No activeAdapter or getEcosystemWalletComponents method, returning null." ); return null; } try { const components = activeAdapter.getEcosystemWalletComponents(); logger4.debug("WalletConnectionUI", "[Debug] walletComponents from adapter:", components); return components; } catch (error) { logger4.error("WalletConnectionUI", "[Debug] Error getting wallet components:", error); setIsError(true); return null; } }, [activeAdapter]); if (!walletComponents) { logger4.debug( "WalletConnectionUI", "[Debug] getEcosystemWalletComponents returned null/undefined, rendering null." ); return null; } logger4.debug("WalletConnectionUI", "Rendering wallet components:", { hasConnectButton: !!walletComponents.ConnectButton, hasAccountDisplay: !!walletComponents.AccountDisplay, hasNetworkSwitcher: !!walletComponents.NetworkSwitcher }); const { ConnectButton, AccountDisplay, NetworkSwitcher } = walletComponents; if (isError) { return /* @__PURE__ */ jsx3("div", { className: cn("flex items-center gap-4", className), children: /* @__PURE__ */ jsx3(Button, { variant: "destructive", size: "sm", onClick: () => window.location.reload(), children: "Wallet Error - Retry" }) }); } if (!walletComponents) { logger4.debug( "WalletConnectionUI", "[Debug] walletComponents is null before rendering, rendering null." ); return null; } return /* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-4", className), children: [ NetworkSwitcher && /* @__PURE__ */ jsx3(NetworkSwitcher, {}), AccountDisplay && /* @__PURE__ */ jsx3(AccountDisplay, {}), ConnectButton && /* @__PURE__ */ jsx3(ConnectButton, {}) ] }); }; // src/components/WalletConnectionHeader.tsx import { jsx as jsx4 } from "react/jsx-runtime"; var WalletConnectionHeader = () => { const { isAdapterLoading, activeAdapter } = useWalletState(); useEffect4(() => { logger5.debug("WalletConnectionHeader", "[Debug] State from useWalletState:", { adapterPresent: !!activeAdapter, adapterNetwork: activeAdapter?.networkConfig.id, isLoading: isAdapterLoading }); }, [activeAdapter, isAdapterLoading]); if (isAdapterLoading) { logger5.debug("WalletConnectionHeader", "[Debug] Adapter loading, showing skeleton."); return /* @__PURE__ */ jsx4("div", { className: "h-9 w-28 animate-pulse rounded bg-muted" }); } return /* @__PURE__ */ jsx4(WalletConnectionUI, {}); }; // src/components/WalletConnectionWithSettings.tsx import { Settings } from "lucide-react"; import { useCallback as useCallback3, useEffect as useEffect5, useState as useState4 } from "react"; import { Button as Button2, NetworkSettingsDialog, useNetworkErrors } from "@openzeppelin/contracts-ui-builder-ui"; import { Fragment, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime"; var WalletConnectionWithSettings = () => { const { isAdapterLoading, activeAdapter, activeNetworkConfig } = useWalletState(); const { setOpenNetworkSettingsHandler } = useNetworkErrors(); const [showNetworkSettings, setShowNetworkSettings] = useState4(false); const [defaultTab, setDefaultTab] = useState4("rpc"); const openNetworkSettings = useCallback3( (networkId, tab = "rpc") => { if (activeNetworkConfig && networkId === activeNetworkConfig.id) { setDefaultTab(tab); setShowNetworkSettings(true); } }, [activeNetworkConfig] ); useEffect5(() => { setOpenNetworkSettingsHandler(openNetworkSettings); }, [openNetworkSettings, setOpenNetworkSettingsHandler]); if (isAdapterLoading) { return /* @__PURE__ */ jsx5("div", { className: "h-9 w-28 animate-pulse rounded bg-muted" }); } return /* @__PURE__ */ jsxs2(Fragment, { children: [ /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2", children: [ /* @__PURE__ */ jsx5(WalletConnectionUI, {}), activeAdapter && activeNetworkConfig && /* @__PURE__ */ jsx5( Button2, { variant: "ghost", size: "icon", className: "h-9 w-9", title: "Network Settings", onClick: () => setShowNetworkSettings(true), children: /* @__PURE__ */ jsx5(Settings, { className: "h-4 w-4" }) } ) ] }), /* @__PURE__ */ jsx5( NetworkSettingsDialog, { isOpen: showNetworkSettings, onOpenChange: setShowNetworkSettings, networkConfig: activeNetworkConfig, adapter: activeAdapter, defaultTab } ) ] }); }; export { AdapterContext, AdapterProvider, WalletConnectionHeader, WalletConnectionUI, WalletConnectionWithSettings, WalletStateContext, WalletStateProvider, useAdapterContext, useDerivedAccountStatus, useDerivedChainInfo, useDerivedConnectStatus, useDerivedDisconnect, useDerivedSwitchChainStatus, useWalletState }; //# sourceMappingURL=index.js.map