|
| 1 | +import { createAppKit } from "@reown/appkit/vue"; |
1 | 2 | import { WagmiAdapter } from "@reown/appkit-adapter-wagmi"; |
2 | 3 |
|
| 4 | +// Singleton initialization state |
| 5 | +let initializationPromise: Promise<void> | null = null; |
| 6 | +let wagmiAdapterInstance: WagmiAdapter | null = null; |
| 7 | + |
| 8 | +/** |
| 9 | + * Creates metadata configuration for AppKit. |
| 10 | + */ |
| 11 | +function createMetadata(origin: string) { |
| 12 | + return { |
| 13 | + name: "ZKsync SSO Auth Server", |
| 14 | + description: "ZKsync SSO Auth Server", |
| 15 | + url: origin, |
| 16 | + icons: [`${origin}/icon-512.png`], |
| 17 | + }; |
| 18 | +} |
| 19 | + |
| 20 | +/** |
| 21 | + * Creates plain chain object to avoid Viem Proxy serialization issues. |
| 22 | + */ |
| 23 | +function createPlainChain(defaultChain: ReturnType<typeof useClientStore>["defaultChain"]) { |
| 24 | + return { |
| 25 | + id: defaultChain.id, |
| 26 | + name: defaultChain.name, |
| 27 | + nativeCurrency: { |
| 28 | + name: defaultChain.nativeCurrency.name, |
| 29 | + symbol: defaultChain.nativeCurrency.symbol, |
| 30 | + decimals: defaultChain.nativeCurrency.decimals, |
| 31 | + }, |
| 32 | + rpcUrls: { |
| 33 | + default: { |
| 34 | + http: [...defaultChain.rpcUrls.default.http], |
| 35 | + }, |
| 36 | + }, |
| 37 | + blockExplorers: defaultChain.blockExplorers |
| 38 | + ? { |
| 39 | + default: { |
| 40 | + name: defaultChain.blockExplorers.default.name, |
| 41 | + url: defaultChain.blockExplorers.default.url, |
| 42 | + }, |
| 43 | + } |
| 44 | + : undefined, |
| 45 | + }; |
| 46 | +} |
| 47 | + |
| 48 | +/** |
| 49 | + * Initializes AppKit with proper singleton pattern to prevent race conditions. |
| 50 | + * Uses Promise-based locking to ensure only one initialization occurs. |
| 51 | + */ |
| 52 | +async function initializeAppKit( |
| 53 | + projectId: string, |
| 54 | + metadata: ReturnType<typeof createMetadata>, |
| 55 | + plainChain: ReturnType<typeof createPlainChain>, |
| 56 | +) { |
| 57 | + if (initializationPromise) { |
| 58 | + // Another initialization is in progress, wait for it |
| 59 | + await initializationPromise; |
| 60 | + return; |
| 61 | + } |
| 62 | + |
| 63 | + if (wagmiAdapterInstance) { |
| 64 | + // Already initialized |
| 65 | + return; |
| 66 | + } |
| 67 | + |
| 68 | + // Create new initialization promise |
| 69 | + initializationPromise = (async () => { |
| 70 | + try { |
| 71 | + wagmiAdapterInstance = new WagmiAdapter({ |
| 72 | + networks: [plainChain], |
| 73 | + projectId, |
| 74 | + }); |
| 75 | + |
| 76 | + createAppKit({ |
| 77 | + adapters: [wagmiAdapterInstance], |
| 78 | + networks: [plainChain], |
| 79 | + projectId, |
| 80 | + metadata, |
| 81 | + }); |
| 82 | + } catch (error) { |
| 83 | + console.warn("Failed to initialize AppKit:", error); |
| 84 | + wagmiAdapterInstance = null; |
| 85 | + throw error; |
| 86 | + } finally { |
| 87 | + initializationPromise = null; |
| 88 | + } |
| 89 | + })(); |
| 90 | + |
| 91 | + await initializationPromise; |
| 92 | +} |
| 93 | + |
| 94 | +/** |
| 95 | + * Composable for accessing AppKit functionality. |
| 96 | + * Implements lazy initialization on first call to avoid SSR issues. |
| 97 | + * |
| 98 | + * IMPORTANT: Due to lazy 'fire-and-forget' initialization: |
| 99 | + * - wagmiConfig and wagmiAdapter will be null/undefined during SSR |
| 100 | + * - They will also be null/undefined on the first client-side call |
| 101 | + * - They will only be available after async initialization completes |
| 102 | + * - All consuming components MUST handle null values gracefully |
| 103 | + * - Components should reactively watch these values or check for null before use |
| 104 | + */ |
3 | 105 | export const useAppKit = () => { |
4 | 106 | const runtimeConfig = useRuntimeConfig(); |
5 | 107 | const { defaultChain } = useClientStore(); |
6 | 108 |
|
7 | 109 | const projectId = runtimeConfig.public.appKitProjectId; |
8 | | - const metadata = { |
9 | | - name: "ZKsync SSO Auth Server", |
10 | | - description: "ZKsync SSO Auth Server", |
11 | | - url: window.location.origin, |
12 | | - icons: [new URL("/icon-512.png", window.location.origin).toString()], |
13 | | - }; |
| 110 | + const origin |
| 111 | + = typeof window !== "undefined" |
| 112 | + ? window.location.origin |
| 113 | + : runtimeConfig.public?.authServerOrigin |
| 114 | + ?? process.env.NUXT_PUBLIC_AUTH_SERVER_ORIGIN |
| 115 | + ?? "https://auth.zksync.dev"; |
14 | 116 |
|
15 | | - const wagmiAdapter = new WagmiAdapter({ |
16 | | - networks: [defaultChain], |
17 | | - projectId, |
18 | | - }); |
| 117 | + const metadata = createMetadata(origin); |
| 118 | + const plainChain = createPlainChain(defaultChain); |
| 119 | + |
| 120 | + // Lazy initialization - only create AppKit when first used on client |
| 121 | + if (typeof window !== "undefined" && !wagmiAdapterInstance && !initializationPromise) { |
| 122 | + // Fire and forget - initialization happens asynchronously |
| 123 | + initializeAppKit(projectId, metadata, plainChain).catch((error) => { |
| 124 | + console.warn("AppKit initialization failed:", error); |
| 125 | + }); |
| 126 | + } |
19 | 127 |
|
20 | | - const wagmiConfig = wagmiAdapter.wagmiConfig; |
| 128 | + const wagmiConfig = wagmiAdapterInstance?.wagmiConfig; |
21 | 129 |
|
22 | 130 | return { |
23 | 131 | metadata, |
24 | 132 | projectId, |
25 | | - wagmiAdapter, |
| 133 | + wagmiAdapter: wagmiAdapterInstance, |
26 | 134 | wagmiConfig, |
27 | 135 | }; |
28 | 136 | }; |
| 137 | + |
| 138 | +// HMR cleanup - reset state on hot module replacement |
| 139 | +if (import.meta.hot) { |
| 140 | + import.meta.hot.dispose(() => { |
| 141 | + initializationPromise = null; |
| 142 | + wagmiAdapterInstance = null; |
| 143 | + }); |
| 144 | +} |
0 commit comments