signer-test-sdk-react 0.0.22 → 0.0.23

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.
Files changed (93) hide show
  1. package/dist/src/AbstraxnProvider.d.ts +5 -13
  2. package/dist/src/AbstraxnProvider.js +14 -3126
  3. package/dist/src/AbstraxnProvider.js.map +1 -1
  4. package/dist/src/ConnectButton.css +1 -1
  5. package/dist/src/ExternalWalletButtons.css +1 -1
  6. package/dist/src/ExternalWalletButtons.js +2 -2
  7. package/dist/src/ExternalWalletButtons.js.map +1 -1
  8. package/dist/src/WalletModal.css +193 -373
  9. package/dist/src/WalletModal.d.ts +1 -1
  10. package/dist/src/WalletModal.js +108 -45
  11. package/dist/src/WalletModal.js.map +1 -1
  12. package/dist/src/chains.d.ts +4 -3
  13. package/dist/src/chains.js +154 -84
  14. package/dist/src/chains.js.map +1 -1
  15. package/dist/src/components/AbstraxnProvider/AbstraxnProvider.d.ts +12 -0
  16. package/dist/src/components/AbstraxnProvider/AbstraxnProvider.js +146 -0
  17. package/dist/src/components/AbstraxnProvider/AbstraxnProvider.js.map +1 -0
  18. package/dist/src/components/AbstraxnProvider/AbstraxnProviderInner.d.ts +25 -0
  19. package/dist/src/components/AbstraxnProvider/AbstraxnProviderInner.js +3086 -0
  20. package/dist/src/components/AbstraxnProvider/AbstraxnProviderInner.js.map +1 -0
  21. package/dist/src/components/AbstraxnProvider/AbstraxnProviderWithWagmi.d.ts +8 -0
  22. package/dist/src/components/AbstraxnProvider/AbstraxnProviderWithWagmi.js +46 -0
  23. package/dist/src/components/AbstraxnProvider/AbstraxnProviderWithWagmi.js.map +1 -0
  24. package/dist/src/components/AbstraxnProvider/AbstraxnProviderWithoutWagmi.d.ts +8 -0
  25. package/dist/src/components/AbstraxnProvider/AbstraxnProviderWithoutWagmi.js +12 -0
  26. package/dist/src/components/AbstraxnProvider/AbstraxnProviderWithoutWagmi.js.map +1 -0
  27. package/dist/src/components/AbstraxnProvider/context.d.ts +2 -0
  28. package/dist/src/components/AbstraxnProvider/context.js +6 -0
  29. package/dist/src/components/AbstraxnProvider/context.js.map +1 -0
  30. package/dist/src/components/AbstraxnProvider/index.d.ts +6 -0
  31. package/dist/src/components/AbstraxnProvider/index.js +7 -0
  32. package/dist/src/components/AbstraxnProvider/index.js.map +1 -0
  33. package/dist/src/components/AbstraxnProvider/useAbstraxnProviderBase.d.ts +30 -0
  34. package/dist/src/components/AbstraxnProvider/useAbstraxnProviderBase.js +49 -0
  35. package/dist/src/components/AbstraxnProvider/useAbstraxnProviderBase.js.map +1 -0
  36. package/dist/src/components/AbstraxnProvider/useAbstraxnWallet.d.ts +2 -0
  37. package/dist/src/components/AbstraxnProvider/useAbstraxnWallet.js +13 -0
  38. package/dist/src/components/AbstraxnProvider/useAbstraxnWallet.js.map +1 -0
  39. package/dist/src/components/AbstraxnProvider/useOAuthCallbacks.d.ts +22 -0
  40. package/dist/src/components/AbstraxnProvider/useOAuthCallbacks.js +242 -0
  41. package/dist/src/components/AbstraxnProvider/useOAuthCallbacks.js.map +1 -0
  42. package/dist/src/components/AbstraxnProvider/useWalletInitialization.d.ts +25 -0
  43. package/dist/src/components/AbstraxnProvider/useWalletInitialization.js +539 -0
  44. package/dist/src/components/AbstraxnProvider/useWalletInitialization.js.map +1 -0
  45. package/dist/src/components/AbstraxnProvider/utils.d.ts +41 -0
  46. package/dist/src/components/AbstraxnProvider/utils.js +139 -0
  47. package/dist/src/components/AbstraxnProvider/utils.js.map +1 -0
  48. package/dist/src/components/OnboardingUI/OnboardingUI.css +8 -5
  49. package/dist/src/components/OnboardingUI/OnboardingUIWeb.js +6 -0
  50. package/dist/src/components/OnboardingUI/OnboardingUIWeb.js.map +1 -1
  51. package/dist/src/components/WalletModal/components/ChainSelector.css +249 -102
  52. package/dist/src/components/WalletModal/components/ChainSelector.d.ts +7 -6
  53. package/dist/src/components/WalletModal/components/ChainSelector.js +68 -27
  54. package/dist/src/components/WalletModal/components/ChainSelector.js.map +1 -1
  55. package/dist/src/components/WalletModal/components/ExportKeyModal.css +89 -88
  56. package/dist/src/components/WalletModal/components/ExportKeyModal.d.ts +6 -1
  57. package/dist/src/components/WalletModal/components/ExportKeyModal.js +6 -11
  58. package/dist/src/components/WalletModal/components/ExportKeyModal.js.map +1 -1
  59. package/dist/src/components/WalletModal/components/ExportWarningModal.css +107 -2
  60. package/dist/src/components/WalletModal/components/ExportWarningModal.d.ts +7 -1
  61. package/dist/src/components/WalletModal/components/ExportWarningModal.js +5 -3
  62. package/dist/src/components/WalletModal/components/ExportWarningModal.js.map +1 -1
  63. package/dist/src/components/WalletModal/components/ManageWalletModal.css +90 -4
  64. package/dist/src/components/WalletModal/components/ManageWalletModal.d.ts +3 -3
  65. package/dist/src/components/WalletModal/components/ManageWalletModal.js +28 -13
  66. package/dist/src/components/WalletModal/components/ManageWalletModal.js.map +1 -1
  67. package/dist/src/components/WalletModal/components/PreviewTransactionModal.css +3 -4
  68. package/dist/src/components/WalletModal/components/ReceiveModal.css +93 -58
  69. package/dist/src/components/WalletModal/components/ReceiveModal.js +1 -1
  70. package/dist/src/components/WalletModal/components/ReceiveModal.js.map +1 -1
  71. package/dist/src/components/WalletModal/components/SendModal.css +170 -127
  72. package/dist/src/components/WalletModal/components/SendModal.d.ts +4 -6
  73. package/dist/src/components/WalletModal/components/SendModal.js +131 -39
  74. package/dist/src/components/WalletModal/components/SendModal.js.map +1 -1
  75. package/dist/src/components/WalletModal/components/SuccessModal.css +7 -8
  76. package/dist/src/components/WalletModal/components/TokenSelectorModal.css +240 -0
  77. package/dist/src/components/WalletModal/components/TokenSelectorModal.d.ts +21 -0
  78. package/dist/src/components/WalletModal/components/TokenSelectorModal.js +44 -0
  79. package/dist/src/components/WalletModal/components/TokenSelectorModal.js.map +1 -0
  80. package/dist/src/components/WalletModal/components/index.d.ts +2 -0
  81. package/dist/src/components/WalletModal/components/index.js +1 -0
  82. package/dist/src/components/WalletModal/components/index.js.map +1 -1
  83. package/dist/src/hooks.d.ts +301 -329
  84. package/dist/src/hooks.js +152 -123
  85. package/dist/src/hooks.js.map +1 -1
  86. package/dist/src/index.d.ts +1 -1
  87. package/dist/src/index.js +1 -1
  88. package/dist/src/index.js.map +1 -1
  89. package/dist/src/wagmiConfig.d.ts +1 -1
  90. package/dist/src/wagmiConfig.js +33 -20
  91. package/dist/src/wagmiConfig.js.map +1 -1
  92. package/dist/tsconfig.tsbuildinfo +1 -1
  93. package/package.json +1 -1
@@ -0,0 +1,3086 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * AbstraxnProviderInner - Core provider logic
4
+ * This component contains all the wallet state management and operations
5
+ */
6
+ import { useEffect, useState, useRef, useCallback, useMemo, } from "react";
7
+ import { AbstraxnWallet, AuthenticationError, } from "signer-test-sdk-core";
8
+ import { OnboardingUIWeb } from "../OnboardingUI";
9
+ import { parseEther, createPublicClient, http } from "viem";
10
+ import { ExternalWalletButtons } from "../../ExternalWalletButtons";
11
+ import { EVM_CHAINS, SOLANA_CHAINS, getChainById, toCoreChain, } from "../../chains";
12
+ import { AbstraxnContext } from "./context";
13
+ export function AbstraxnProviderInner({ config, children, base, wagmi, }) {
14
+ const { isInitialized, setIsInitialized, isConnected, setIsConnected, address, setAddress, user, setUser, whoami, setWhoami, chainId, setChainId, error, setError, loading, setLoading, walletBalance, setWalletBalance, onboardingRef, otpIdRef, walletRef, googleCallbackHandledRef, twitterCallbackHandledRef, discordCallbackHandledRef, } = base;
15
+ const externalWalletsEnabled = config.externalWallets?.enabled ?? false;
16
+ // Keep a ref to avoid re-creating callbacks when toggling config (prevents flicker)
17
+ const externalWalletsEnabledRef = useRef(externalWalletsEnabled);
18
+ useEffect(() => {
19
+ externalWalletsEnabledRef.current = externalWalletsEnabled;
20
+ }, [externalWalletsEnabled]);
21
+ const wagmiAccount = wagmi?.wagmiAccount || null;
22
+ const wagmiConnect = wagmi?.wagmiConnect || null;
23
+ const wagmiDisconnect = wagmi?.wagmiDisconnect || null;
24
+ const wagmiSignMessage = wagmi?.wagmiSignMessage || null;
25
+ const wagmiSendTransaction = wagmi?.wagmiSendTransaction || null;
26
+ const wagmiSwitchChain = wagmi?.wagmiSwitchChain || null;
27
+ const wagmiBalance = wagmi?.wagmiBalance || null;
28
+ const wagmiChainIdHook = wagmi?.wagmiChainIdHook || null;
29
+ // External wallet state
30
+ const [isExternalWalletConnected, setIsExternalWalletConnected] = useState(false);
31
+ const [externalWalletAddress, setExternalWalletAddress] = useState(null);
32
+ const [externalWalletChainId, setExternalWalletChainId] = useState(null);
33
+ // Connection type tracking: 'google' | 'email' | 'discord' | 'x' | 'passkey' | 'metamask' | 'walletconnect' | 'coinbase' | 'phantom' | 'injected' | null
34
+ const [connectionType, setConnectionType] = useState(null);
35
+ const explicitConnectionRef = useRef(false);
36
+ const autoDisconnectHandledRef = useRef(false);
37
+ // Track when we last connected to prevent premature reset
38
+ const lastConnectionTimeRef = useRef(0);
39
+ // Refs to track previous values and prevent unnecessary updates
40
+ const lastAddressRef = useRef(null);
41
+ const lastChainIdRef = useRef(null);
42
+ const isUpdatingRef = useRef(false);
43
+ const connectionTimeoutRef = useRef(null);
44
+ // Track if we're in the middle of a disconnect to prevent useEffect from interfering
45
+ const disconnectingRef = useRef(false);
46
+ // Show/hide external wallet UI when config changes (prevents modal flicker)
47
+ useEffect(() => {
48
+ const onboardingAny = onboardingRef.current;
49
+ if (!onboardingAny)
50
+ return;
51
+ requestAnimationFrame(() => {
52
+ if (externalWalletsEnabled) {
53
+ // Show external wallet container when enabled
54
+ if (onboardingAny.externalWalletContainer) {
55
+ onboardingAny.externalWalletContainer.style.display = "";
56
+ }
57
+ // Show divider if email/Google are also visible
58
+ const authMethods = onboardingAny.config?.authMethods || [
59
+ "otp",
60
+ "google",
61
+ ];
62
+ const showEmail = authMethods.includes("otp");
63
+ const showGoogle = authMethods.includes("google");
64
+ const hasEmailOrGoogle = showEmail || showGoogle;
65
+ if (onboardingAny.externalWalletDivider && hasEmailOrGoogle) {
66
+ onboardingAny.externalWalletDivider.style.display = "";
67
+ }
68
+ }
69
+ else {
70
+ // Hide external wallet container when disabled
71
+ if (onboardingAny.externalWalletContainer) {
72
+ onboardingAny.externalWalletContainer.style.display = "none";
73
+ }
74
+ if (onboardingAny.externalWalletDivider) {
75
+ onboardingAny.externalWalletDivider.style.display = "none";
76
+ }
77
+ }
78
+ });
79
+ }, [externalWalletsEnabled]);
80
+ // Restore external wallet connection on mount if wagmi has already restored it
81
+ // This runs after wagmi has had time to restore from localStorage
82
+ useEffect(() => {
83
+ if (!externalWalletsEnabled || !wagmiAccount) {
84
+ return;
85
+ }
86
+ // Check if wagmi has already restored a connection from localStorage
87
+ // This happens automatically with wagmi's persistence when storage is configured
88
+ const checkAndRestore = () => {
89
+ if (wagmiAccount.isConnected &&
90
+ wagmiAccount.address &&
91
+ !isExternalWalletConnected) {
92
+ const walletAddress = wagmiAccount.address.toLowerCase();
93
+ const currentChainId = wagmiChainIdHook || wagmiAccount.chainId || null;
94
+ // Set explicitConnectionRef FIRST to prevent auto-disconnect from interfering
95
+ explicitConnectionRef.current = true;
96
+ autoDisconnectHandledRef.current = false;
97
+ // Restore external wallet state
98
+ setIsExternalWalletConnected(true);
99
+ setExternalWalletAddress(walletAddress);
100
+ setAddress(walletAddress);
101
+ setIsConnected(true);
102
+ setExternalWalletChainId(currentChainId);
103
+ setChainId(currentChainId);
104
+ // Restore connection type from connector
105
+ const connector = wagmiAccount.connector;
106
+ if (connector) {
107
+ const connectorId = connector.id.toLowerCase();
108
+ const connectorName = connector.name?.toLowerCase() || "";
109
+ // Map connector to connection type
110
+ if (connectorId.includes("metamask") ||
111
+ connectorId.includes("io.metamask") ||
112
+ connectorName.includes("metamask")) {
113
+ setConnectionType("metamask");
114
+ }
115
+ else if (connectorId.includes("walletconnect") ||
116
+ connectorId.includes("wallet_connect") ||
117
+ connectorName.includes("walletconnect")) {
118
+ setConnectionType("walletconnect");
119
+ }
120
+ else {
121
+ // For other wallets (coinbase, phantom, injected), store as-is
122
+ // The hook will normalize them, but we keep the original for reference
123
+ const fallbackType = connectorName || connectorId;
124
+ setConnectionType(fallbackType);
125
+ }
126
+ }
127
+ else {
128
+ // Try to restore from localStorage as fallback
129
+ try {
130
+ const storedType = localStorage.getItem("abstraxn_connection_type");
131
+ if (storedType) {
132
+ setConnectionType(storedType);
133
+ }
134
+ }
135
+ catch (e) {
136
+ // Ignore localStorage errors
137
+ }
138
+ }
139
+ // Update refs to track the restored connection
140
+ lastAddressRef.current = walletAddress;
141
+ lastChainIdRef.current = currentChainId;
142
+ lastConnectionTimeRef.current = Date.now();
143
+ return true; // Indicate restoration happened
144
+ }
145
+ return false;
146
+ };
147
+ // Check immediately
148
+ if (checkAndRestore()) {
149
+ return; // Already restored
150
+ }
151
+ // Also check after delays to catch wagmi's async restoration
152
+ // Wagmi might restore the connection asynchronously after the component mounts
153
+ let timeout2 = null;
154
+ const timeout1 = setTimeout(() => {
155
+ if (!checkAndRestore()) {
156
+ // Check one more time after a longer delay
157
+ timeout2 = setTimeout(() => {
158
+ checkAndRestore();
159
+ }, 300);
160
+ }
161
+ }, 150);
162
+ return () => {
163
+ clearTimeout(timeout1);
164
+ if (timeout2) {
165
+ clearTimeout(timeout2);
166
+ }
167
+ };
168
+ }, [
169
+ externalWalletsEnabled,
170
+ wagmiAccount?.isConnected,
171
+ wagmiAccount?.address,
172
+ wagmiChainIdHook,
173
+ isExternalWalletConnected,
174
+ ]); // Re-run if wagmi state changes
175
+ // Initialize wallet
176
+ useEffect(() => {
177
+ // If wallet already exists (from previous mount), reuse it to prevent flicker
178
+ if (walletRef.current) {
179
+ return;
180
+ }
181
+ // Compute supported chains from chain config (outside useMemo since we're in useEffect)
182
+ // This should include ALL chains that might be shown in the UI (availableChains)
183
+ // We use the same logic as availableChains to ensure consistency
184
+ let computedSupportedChains = undefined;
185
+ if (config.supportedChains) {
186
+ // Use explicitly provided chains
187
+ computedSupportedChains = config.supportedChains;
188
+ }
189
+ else {
190
+ // Compute from chains config - use same logic as availableChains
191
+ const chainConfig = config.chains;
192
+ const chains = [];
193
+ // Priority 1: Check if chains is an array of viem chain objects (direct format)
194
+ if (Array.isArray(chainConfig) && chainConfig?.length > 0) {
195
+ const firstItem = chainConfig[0];
196
+ if (firstItem &&
197
+ typeof firstItem.id === "number" &&
198
+ firstItem.nativeCurrency) {
199
+ // Convert viem chain objects to core chain format
200
+ chainConfig.forEach((viemChain) => {
201
+ let rpcUrl = "";
202
+ if (viemChain.rpcUrls?.default?.http) {
203
+ rpcUrl = Array.isArray(viemChain.rpcUrls.default.http)
204
+ ? viemChain.rpcUrls.default.http[0]
205
+ : viemChain.rpcUrls.default.http;
206
+ }
207
+ else if (viemChain.rpcUrls?.public?.http) {
208
+ rpcUrl = Array.isArray(viemChain.rpcUrls.public.http)
209
+ ? viemChain.rpcUrls.public.http[0]
210
+ : viemChain.rpcUrls.public.http;
211
+ }
212
+ chains.push({
213
+ id: viemChain.id,
214
+ name: viemChain.name,
215
+ rpcUrl: rpcUrl ||
216
+ `https://rpc.ankr.com/${viemChain.name.toLowerCase()}`,
217
+ nativeCurrency: viemChain.nativeCurrency,
218
+ });
219
+ });
220
+ }
221
+ }
222
+ // Priority 2: Check new chains config format
223
+ else if (chainConfig?.supportedEvmChains &&
224
+ chainConfig.supportedEvmChains.length > 0) {
225
+ chainConfig.supportedEvmChains.forEach((chainName) => {
226
+ const chainData = EVM_CHAINS[chainName];
227
+ if (chainData && chainData.type === "evm") {
228
+ chains.push(toCoreChain(chainData));
229
+ }
230
+ });
231
+ }
232
+ // Priority 3: If no specific config, include default chains that might be shown
233
+ // This ensures Base and other common chains are available even if not explicitly configured
234
+ if (chains.length === 0) {
235
+ // Include common chains that are likely to be in availableChains
236
+ chains.push(toCoreChain(EVM_CHAINS.ethereum));
237
+ chains.push(toCoreChain(EVM_CHAINS.polygon));
238
+ chains.push(toCoreChain(EVM_CHAINS.base));
239
+ }
240
+ computedSupportedChains =
241
+ chains.length > 0
242
+ ? chains
243
+ : [
244
+ toCoreChain(EVM_CHAINS.ethereum),
245
+ toCoreChain(EVM_CHAINS.polygon),
246
+ toCoreChain(EVM_CHAINS.base),
247
+ ];
248
+ }
249
+ const walletInstance = new AbstraxnWallet({
250
+ apiKey: config.apiKey,
251
+ authMethods: config.authMethods,
252
+ googleClientId: config.googleClientId,
253
+ defaultChainId: config.chains?.defaultChainId || config.defaultChainId,
254
+ supportedChains: computedSupportedChains,
255
+ autoConnect: config.autoConnect ?? false,
256
+ enableLogging: config.enableLogging ?? false,
257
+ });
258
+ walletRef.current = walletInstance;
259
+ // Set up event listeners
260
+ walletInstance.on("connect", async () => {
261
+ try {
262
+ // Verify whoami exists before setting connected
263
+ const whoamiInfo = await walletInstance.getWhoami();
264
+ if (!whoamiInfo) {
265
+ // If whoami is not available, don't set connected
266
+ return;
267
+ }
268
+ setIsConnected(true);
269
+ const userInfo = await walletInstance.getUserInfo();
270
+ setUser(userInfo);
271
+ setWhoami(whoamiInfo);
272
+ // Try to get address, but don't fail if not available yet
273
+ try {
274
+ const addr = await walletInstance.getAddress();
275
+ setAddress(addr);
276
+ }
277
+ catch (addrErr) {
278
+ console.warn("Address not available on connect:", addrErr);
279
+ }
280
+ const cid = await walletInstance.getChainId();
281
+ setChainId(cid);
282
+ // Always restore connection type from localStorage on connect
283
+ // This ensures connectionType is available after OAuth redirects (Google, X, Discord)
284
+ // and after page refreshes
285
+ try {
286
+ const storedType = localStorage.getItem("abstraxn_connection_type");
287
+ if (storedType &&
288
+ (storedType === "google" ||
289
+ storedType === "email" ||
290
+ storedType === "discord" ||
291
+ storedType === "x" ||
292
+ storedType === "passkey")) {
293
+ setConnectionType(storedType);
294
+ }
295
+ }
296
+ catch (e) {
297
+ // Ignore localStorage errors
298
+ }
299
+ }
300
+ catch (err) {
301
+ console.error("Error getting wallet info on connect:", err);
302
+ }
303
+ // Ensure loading modal is closed when connected (whoami received)
304
+ if (onboardingRef.current) {
305
+ const onboardingAny = onboardingRef.current;
306
+ if (onboardingAny.hideLoadingModal) {
307
+ onboardingAny.hideLoadingModal();
308
+ }
309
+ // Also ensure the overlay is hidden if it was opened
310
+ if (onboardingAny.modalOverlay &&
311
+ onboardingAny.modalOverlay.classList.contains("onboarding-modal-open")) {
312
+ onboardingAny.modalOverlay.classList.remove("onboarding-modal-open");
313
+ onboardingAny.modalOverlay.classList.add("onboarding-modal-closing");
314
+ setTimeout(() => {
315
+ if (onboardingAny.modalOverlay) {
316
+ onboardingAny.modalOverlay.style.display = "none";
317
+ }
318
+ }, 200);
319
+ document.body.style.overflow = "";
320
+ }
321
+ }
322
+ });
323
+ walletInstance.on("disconnect", () => {
324
+ setIsConnected(false);
325
+ setAddress(null);
326
+ setUser(null);
327
+ setWhoami(null);
328
+ setChainId(null);
329
+ setConnectionType(null);
330
+ // Clear from localStorage
331
+ try {
332
+ localStorage.removeItem("abstraxn_connection_type");
333
+ }
334
+ catch (e) {
335
+ // Ignore localStorage errors
336
+ }
337
+ });
338
+ walletInstance.on("accountChanged", (newAddress) => {
339
+ setAddress(newAddress);
340
+ });
341
+ walletInstance.on("chainChanged", (newChainId) => {
342
+ setChainId(newChainId);
343
+ });
344
+ // Initialize OnboardingUI only if not already created (prevents flicker on config change)
345
+ if (onboardingRef.current) {
346
+ return;
347
+ }
348
+ const uiConfig = config.ui || {};
349
+ const onboarding = new OnboardingUIWeb({
350
+ logo: uiConfig.logo,
351
+ theme: uiConfig.theme || "light",
352
+ showFooter: uiConfig.showFooter !== false,
353
+ onboardTitle: uiConfig.onboardTitle || "Sign In",
354
+ modal: uiConfig.modal !== false,
355
+ closeOnBackdropClick: uiConfig.closeOnBackdropClick !== false,
356
+ className: uiConfig.className,
357
+ style: uiConfig.style,
358
+ labels: uiConfig.labels,
359
+ colors: uiConfig.colors,
360
+ customCSS: uiConfig.customCSS,
361
+ authMethods: uiConfig.authMethods || config.authMethods,
362
+ onEmailOtpInitiate: async (email) => {
363
+ try {
364
+ if (!walletInstance)
365
+ throw new Error("Wallet not initialized");
366
+ const authManager = walletInstance.getAuthManager();
367
+ const connectionTypeValue = "email";
368
+ setConnectionType(connectionTypeValue);
369
+ try {
370
+ localStorage.setItem("abstraxn_connection_type", connectionTypeValue);
371
+ }
372
+ catch (e) {
373
+ // Ignore localStorage errors
374
+ }
375
+ const result = await authManager.loginWithOTP(email);
376
+ otpIdRef.current = result.otpId;
377
+ }
378
+ catch (err) {
379
+ console.error("OTP Init Error:", err);
380
+ throw err;
381
+ }
382
+ },
383
+ onEmailOtpVerify: async (_email, otp) => {
384
+ try {
385
+ if (!walletInstance)
386
+ throw new Error("Wallet not initialized");
387
+ if (!otpIdRef.current)
388
+ throw new Error("OTP ID not found");
389
+ // Set connection type for email OTP before verification
390
+ const connectionTypeValue = "email";
391
+ setConnectionType(connectionTypeValue);
392
+ try {
393
+ localStorage.setItem("abstraxn_connection_type", connectionTypeValue);
394
+ }
395
+ catch (e) {
396
+ // Ignore localStorage errors
397
+ }
398
+ const authManager = walletInstance.getAuthManager();
399
+ const user = await authManager.verifyOTP(otpIdRef.current, otp);
400
+ otpIdRef.current = null;
401
+ // Connect wallet after successful authentication
402
+ await walletInstance.connect();
403
+ // Clear any previous errors on successful verification
404
+ setError(null);
405
+ return { success: true, user };
406
+ }
407
+ catch (err) {
408
+ console.error("OTP Verify Error:", err);
409
+ // Return the actual error message from the API
410
+ const errorMessage = err instanceof Error ? err.message : "Failed to verify OTP";
411
+ return { success: false, error: errorMessage };
412
+ }
413
+ },
414
+ onGoogleLogin: async () => {
415
+ if (!walletInstance)
416
+ throw new Error("Wallet not initialized");
417
+ const connectionTypeValue = "google";
418
+ setConnectionType(connectionTypeValue);
419
+ // Store in localStorage for restoration
420
+ try {
421
+ localStorage.setItem("abstraxn_connection_type", connectionTypeValue);
422
+ localStorage.setItem("abstraxn_oauth_pending", "google");
423
+ }
424
+ catch (e) {
425
+ // Ignore localStorage errors
426
+ }
427
+ // Clear existing query params before starting new flow
428
+ if (typeof window !== "undefined") {
429
+ const url = new URL(window.location.href);
430
+ // Remove auth params that might be present from previous attempts
431
+ url.searchParams.delete("code");
432
+ url.searchParams.delete("state");
433
+ url.searchParams.delete("error");
434
+ url.searchParams.delete("success");
435
+ url.searchParams.delete("accessToken");
436
+ url.searchParams.delete("provider");
437
+ url.searchParams.delete("authProvider");
438
+ window.history.replaceState({}, document.title, url.toString());
439
+ }
440
+ const authManager = walletInstance.getAuthManager();
441
+ await authManager.loginWithGoogle();
442
+ },
443
+ onTwitterLogin: async () => {
444
+ if (!walletInstance)
445
+ throw new Error("Wallet not initialized");
446
+ const connectionTypeValue = "x";
447
+ setConnectionType(connectionTypeValue);
448
+ try {
449
+ localStorage.setItem("abstraxn_connection_type", connectionTypeValue);
450
+ localStorage.setItem("abstraxn_oauth_pending", "twitter");
451
+ }
452
+ catch (e) {
453
+ // Ignore localStorage errors
454
+ }
455
+ // Clear existing query params before starting new flow
456
+ if (typeof window !== "undefined") {
457
+ const url = new URL(window.location.href);
458
+ // Remove auth params that might be present from previous attempts
459
+ url.searchParams.delete("code");
460
+ url.searchParams.delete("state");
461
+ url.searchParams.delete("error");
462
+ url.searchParams.delete("success");
463
+ url.searchParams.delete("accessToken");
464
+ url.searchParams.delete("provider");
465
+ url.searchParams.delete("authProvider");
466
+ window.history.replaceState({}, document.title, url.toString());
467
+ }
468
+ const authManager = walletInstance.getAuthManager();
469
+ await authManager.loginWithTwitter();
470
+ },
471
+ onDiscordLogin: async () => {
472
+ if (!walletInstance)
473
+ throw new Error("Wallet not initialized");
474
+ const connectionTypeValue = "discord";
475
+ setConnectionType(connectionTypeValue);
476
+ try {
477
+ localStorage.setItem("abstraxn_connection_type", connectionTypeValue);
478
+ localStorage.setItem("abstraxn_oauth_pending", "discord");
479
+ }
480
+ catch (e) {
481
+ // Ignore localStorage errors
482
+ }
483
+ // Clear existing query params before starting new flow
484
+ if (typeof window !== "undefined") {
485
+ const url = new URL(window.location.href);
486
+ // Remove auth params that might be present from previous attempts
487
+ url.searchParams.delete("code");
488
+ url.searchParams.delete("state");
489
+ url.searchParams.delete("error");
490
+ url.searchParams.delete("success");
491
+ url.searchParams.delete("accessToken");
492
+ url.searchParams.delete("provider");
493
+ url.searchParams.delete("authProvider");
494
+ window.history.replaceState({}, document.title, url.toString());
495
+ }
496
+ const authManager = walletInstance.getAuthManager();
497
+ await authManager.loginWithDiscord();
498
+ },
499
+ onPasskeyLogin: async () => {
500
+ if (!walletInstance)
501
+ throw new Error("Wallet not initialized");
502
+ const connectionTypeValue = "passkey";
503
+ setConnectionType(connectionTypeValue);
504
+ try {
505
+ localStorage.setItem("abstraxn_connection_type", connectionTypeValue);
506
+ }
507
+ catch (e) {
508
+ // Ignore localStorage errors
509
+ }
510
+ const authManager = walletInstance.getAuthManager();
511
+ const user = await authManager.loginWithPasskey();
512
+ // Set user immediately so onLoginSuccess can access it
513
+ setUser(user);
514
+ // Connect wallet after successful authentication
515
+ await walletInstance.connect();
516
+ },
517
+ onPasskeySignup: async () => {
518
+ if (!walletInstance)
519
+ throw new Error("Wallet not initialized");
520
+ const connectionTypeValue = "passkey";
521
+ setConnectionType(connectionTypeValue);
522
+ try {
523
+ localStorage.setItem("abstraxn_connection_type", connectionTypeValue);
524
+ }
525
+ catch (e) {
526
+ // Ignore localStorage errors
527
+ }
528
+ const authManager = walletInstance.getAuthManager();
529
+ const user = await authManager.signupWithPasskey();
530
+ // Clear any previous errors on successful verification
531
+ // setError(null);
532
+ setUser(user);
533
+ // Connect wallet after successful authentication
534
+ await walletInstance.connect();
535
+ },
536
+ onLoginSuccess: async (_data) => {
537
+ // Clear any previous errors on successful login
538
+ setError(null);
539
+ // Restore connection type from localStorage for OAuth logins (Google, X, Discord)
540
+ // This ensures connectionType is set after OAuth redirects
541
+ try {
542
+ const storedType = localStorage.getItem("abstraxn_connection_type");
543
+ if (storedType &&
544
+ (storedType === "google" ||
545
+ storedType === "x" ||
546
+ storedType === "discord" ||
547
+ storedType === "email" ||
548
+ storedType === "passkey")) {
549
+ setConnectionType(storedType);
550
+ }
551
+ }
552
+ catch (e) {
553
+ // Ignore localStorage errors
554
+ }
555
+ // Load whoami after successful login
556
+ if (walletInstance) {
557
+ try {
558
+ const whoamiInfo = await walletInstance.getWhoami();
559
+ setWhoami(whoamiInfo);
560
+ const addr = await walletInstance.getAddress();
561
+ setAddress(addr);
562
+ const cid = await walletInstance.getChainId();
563
+ setChainId(cid);
564
+ // Only hide onboarding UI and set connected if whoami succeeds
565
+ if (onboardingRef.current) {
566
+ const onboardingAny = onboardingRef.current;
567
+ if (onboardingAny.hideLoadingModal) {
568
+ onboardingAny.hideLoadingModal();
569
+ }
570
+ if (onboardingAny.modalOverlay) {
571
+ onboardingAny.modalOverlay.classList.remove("onboarding-modal-open");
572
+ onboardingAny.modalOverlay.classList.add("onboarding-modal-closing");
573
+ setTimeout(() => {
574
+ if (onboardingAny.modalOverlay) {
575
+ onboardingAny.modalOverlay.style.display = "none";
576
+ }
577
+ }, 200);
578
+ }
579
+ document.body.style.overflow = "";
580
+ }
581
+ // Set connected state only after whoami succeeds
582
+ setIsConnected(true);
583
+ }
584
+ catch (err) {
585
+ console.error("Error loading whoami after login:", err);
586
+ // Hide loading modal on whoami error
587
+ if (onboardingRef.current) {
588
+ const onboardingAny = onboardingRef.current;
589
+ if (onboardingAny.hideLoadingModal) {
590
+ onboardingAny.hideLoadingModal();
591
+ }
592
+ // Show error modal/screen
593
+ const errorMessage = err instanceof Error
594
+ ? err.message
595
+ : "Failed to load user information";
596
+ if (onboardingAny.showError) {
597
+ onboardingAny.showError(errorMessage);
598
+ }
599
+ }
600
+ // Set error state
601
+ const error = err instanceof Error
602
+ ? err
603
+ : new Error("Failed to load user information after login");
604
+ setError(error);
605
+ // Don't set connected if whoami fails
606
+ setIsConnected(false);
607
+ }
608
+ }
609
+ },
610
+ onLoginError: (err) => {
611
+ setError(err);
612
+ },
613
+ }, externalWalletsEnabled); // Pass externalWalletsEnabled as second parameter
614
+ // Inject custom CSS if provided
615
+ if (uiConfig.customCSS) {
616
+ const styleId = "abstraxn-custom-css";
617
+ if (!document.getElementById(styleId)) {
618
+ const style = document.createElement("style");
619
+ style.id = styleId;
620
+ style.textContent = uiConfig.customCSS;
621
+ document.head.appendChild(style);
622
+ }
623
+ }
624
+ const hasAuthParams = (params) => params.get("success") === "true" ||
625
+ params.get("error") ||
626
+ params.get("code") ||
627
+ params.get("accessToken");
628
+ const getAuthProviderFromToken = (token) => {
629
+ try {
630
+ const base64Url = token.split(".")[1];
631
+ const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
632
+ const jsonPayload = decodeURIComponent(window
633
+ .atob(base64)
634
+ .split("")
635
+ .map(function (c) {
636
+ return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
637
+ })
638
+ .join(""));
639
+ const payload = JSON.parse(jsonPayload);
640
+ return (payload.authProvider || payload.provider || "").toLowerCase();
641
+ }
642
+ catch (e) {
643
+ return null;
644
+ }
645
+ };
646
+ const matchesProvider = (provider, params) => {
647
+ const providerParam = (params.get("provider") ||
648
+ params.get("authProvider") ||
649
+ "").toLowerCase();
650
+ const path = window.location.pathname.toLowerCase();
651
+ if (providerParam) {
652
+ if (provider === "twitter") {
653
+ return providerParam === "twitter" || providerParam === "x";
654
+ }
655
+ return providerParam === provider;
656
+ }
657
+ // Check accessToken for provider
658
+ const accessToken = params.get("accessToken");
659
+ if (accessToken) {
660
+ const tokenProvider = getAuthProviderFromToken(accessToken);
661
+ if (tokenProvider) {
662
+ if (provider === "twitter") {
663
+ return tokenProvider === "twitter" || tokenProvider === "x";
664
+ }
665
+ return tokenProvider === provider;
666
+ }
667
+ }
668
+ if (provider === "twitter") {
669
+ return path.includes("/twitter") || path.includes("/x");
670
+ }
671
+ return path.includes(`/${provider}`);
672
+ };
673
+ try {
674
+ onboarding.init();
675
+ }
676
+ catch (err) {
677
+ console.error("Failed to initialize OnboardingUI:", err);
678
+ }
679
+ // Check if we should keep it open (if handling a callback)
680
+ const urlParams = new URLSearchParams(window.location.search);
681
+ const shouldKeepOpen = hasAuthParams(urlParams);
682
+ const hasSuccess = urlParams.get("success") === "true";
683
+ // Hide immediately - use synchronous approach to prevent flicker
684
+ // Only hide if NOT handling a callback
685
+ const onboardingAny = onboarding;
686
+ if (!shouldKeepOpen && onboardingAny.modalOverlay) {
687
+ // Hide immediately without delay to prevent flicker
688
+ onboardingAny.modalOverlay.classList.remove("onboarding-modal-open");
689
+ onboardingAny.modalOverlay.style.display = "none";
690
+ document.body.style.overflow = "";
691
+ }
692
+ else if (hasSuccess) {
693
+ // If success=true is in URL, show loading modal
694
+ // Use setTimeout to ensure onboarding is fully initialized
695
+ setTimeout(() => {
696
+ const onboardingInstance = onboardingRef.current;
697
+ if (onboardingInstance && onboardingInstance.showLoadingModal) {
698
+ // Directly show loading modal - it creates its own overlay with header and footer
699
+ onboardingInstance.showLoadingModal();
700
+ }
701
+ }, 150);
702
+ }
703
+ onboardingRef.current = onboarding;
704
+ // Handle Google OAuth callback (only in useEffect, not exposed)
705
+ const handleGoogleCallback = async (force = false) => {
706
+ if (googleCallbackHandledRef.current)
707
+ return;
708
+ const urlParams = new URLSearchParams(window.location.search);
709
+ if (!hasAuthParams(urlParams) ||
710
+ matchesProvider("twitter", urlParams) ||
711
+ matchesProvider("discord", urlParams)) {
712
+ return;
713
+ }
714
+ // Check if we initiated this OAuth flow
715
+ let oauthPending = null;
716
+ try {
717
+ oauthPending = localStorage.getItem("abstraxn_oauth_pending");
718
+ }
719
+ catch (e) { }
720
+ if (!force && !oauthPending)
721
+ return;
722
+ // Clear the pending flag
723
+ try {
724
+ localStorage.removeItem("abstraxn_oauth_pending");
725
+ }
726
+ catch (e) { }
727
+ // Show loading modal if success=true is in URL
728
+ const hasSuccess = urlParams.get("success") === "true";
729
+ if (hasSuccess && onboardingRef.current) {
730
+ const onboardingAny = onboardingRef.current;
731
+ if (onboardingAny.showLoadingScreen) {
732
+ onboardingAny.showLoadingScreen();
733
+ }
734
+ else if (onboardingAny.showLoadingModal) {
735
+ onboardingAny.showLoadingModal();
736
+ }
737
+ }
738
+ googleCallbackHandledRef.current = true;
739
+ try {
740
+ const user = await walletInstance.handleGoogleCallback();
741
+ if (user) {
742
+ setUser(user);
743
+ window.history.replaceState({}, document.title, window.location.pathname);
744
+ // Hide loading modal and close modal on success
745
+ if (onboardingRef.current) {
746
+ const onboardingAny = onboardingRef.current;
747
+ if (onboardingAny.hideLoadingModal) {
748
+ onboardingAny.hideLoadingModal();
749
+ }
750
+ onboardingRef.current.close();
751
+ }
752
+ }
753
+ }
754
+ catch (err) {
755
+ console.error("Google callback error:", err);
756
+ // Hide loading modal on error (including whoami API failures)
757
+ if (onboardingRef.current) {
758
+ const onboardingAny = onboardingRef.current;
759
+ // Always try to hide loading modal first
760
+ if (onboardingAny.hideLoadingModal) {
761
+ onboardingAny.hideLoadingModal();
762
+ }
763
+ // Also hide any main modal overlay
764
+ if (onboardingAny.modalOverlay) {
765
+ onboardingAny.modalOverlay.style.display = "none";
766
+ }
767
+ // Show error modal/screen
768
+ const errorMessage = err instanceof Error ? err.message : "Google authentication failed";
769
+ if (onboardingAny.showError) {
770
+ onboardingAny.showError(errorMessage);
771
+ }
772
+ }
773
+ setError(err instanceof Error ? err : new Error("Google callback failed"));
774
+ googleCallbackHandledRef.current = false;
775
+ // Ensure body scroll is restored
776
+ document.body.style.overflow = "";
777
+ }
778
+ };
779
+ const handleDiscordCallback = async () => {
780
+ if (discordCallbackHandledRef.current)
781
+ return;
782
+ const urlParams = new URLSearchParams(window.location.search);
783
+ if (!hasAuthParams(urlParams) || !matchesProvider("discord", urlParams)) {
784
+ return;
785
+ }
786
+ // Check if we initiated this OAuth flow
787
+ let oauthPending = null;
788
+ try {
789
+ oauthPending = localStorage.getItem("abstraxn_oauth_pending");
790
+ }
791
+ catch (e) { }
792
+ if (!oauthPending)
793
+ return;
794
+ // Clear the pending flag
795
+ try {
796
+ localStorage.removeItem("abstraxn_oauth_pending");
797
+ }
798
+ catch (e) { }
799
+ // Show loading modal if success=true is in URL
800
+ const hasSuccess = urlParams.get("success") === "true";
801
+ if (hasSuccess && onboardingRef.current) {
802
+ const onboardingAny = onboardingRef.current;
803
+ if (onboardingAny.showLoadingScreen) {
804
+ onboardingAny.showLoadingScreen();
805
+ }
806
+ else if (onboardingAny.showLoadingModal) {
807
+ onboardingAny.showLoadingModal();
808
+ }
809
+ }
810
+ discordCallbackHandledRef.current = true;
811
+ try {
812
+ const user = await walletInstance.handleDiscordCallback();
813
+ if (user) {
814
+ setUser(user);
815
+ window.history.replaceState({}, document.title, window.location.pathname);
816
+ // Hide loading modal and close modal on success
817
+ if (onboardingRef.current) {
818
+ const onboardingAny = onboardingRef.current;
819
+ if (onboardingAny.hideLoadingModal) {
820
+ onboardingAny.hideLoadingModal();
821
+ }
822
+ onboardingRef.current.close();
823
+ }
824
+ }
825
+ }
826
+ catch (err) {
827
+ console.error("Discord callback error:", err);
828
+ // Hide loading modal on error (including whoami API failures)
829
+ if (onboardingRef.current) {
830
+ const onboardingAny = onboardingRef.current;
831
+ // Always try to hide loading modal first
832
+ if (onboardingAny.hideLoadingModal) {
833
+ onboardingAny.hideLoadingModal();
834
+ }
835
+ // Also hide any main modal overlay
836
+ if (onboardingAny.modalOverlay) {
837
+ onboardingAny.modalOverlay.style.display = "none";
838
+ }
839
+ // Show error modal/screen
840
+ const errorMessage = err instanceof Error
841
+ ? err.message
842
+ : "Discord authentication failed";
843
+ if (onboardingAny.showError) {
844
+ onboardingAny.showError(errorMessage);
845
+ }
846
+ }
847
+ setError(err instanceof Error ? err : new Error("Discord callback failed"));
848
+ discordCallbackHandledRef.current = false;
849
+ // Ensure body scroll is restored
850
+ document.body.style.overflow = "";
851
+ }
852
+ };
853
+ const handleTwitterCallback = async () => {
854
+ if (twitterCallbackHandledRef.current)
855
+ return;
856
+ const urlParams = new URLSearchParams(window.location.search);
857
+ if (!hasAuthParams(urlParams) || !matchesProvider("twitter", urlParams)) {
858
+ return;
859
+ }
860
+ // Check if we initiated this OAuth flow
861
+ let oauthPending = null;
862
+ try {
863
+ oauthPending = localStorage.getItem("abstraxn_oauth_pending");
864
+ }
865
+ catch (e) { }
866
+ if (!oauthPending)
867
+ return;
868
+ // Clear the pending flag
869
+ try {
870
+ localStorage.removeItem("abstraxn_oauth_pending");
871
+ }
872
+ catch (e) { }
873
+ // Show loading modal if success=true is in URL
874
+ const hasSuccess = urlParams.get("success") === "true";
875
+ if (hasSuccess && onboardingRef.current) {
876
+ const onboardingAny = onboardingRef.current;
877
+ if (onboardingAny.showLoadingScreen) {
878
+ onboardingAny.showLoadingScreen();
879
+ }
880
+ else if (onboardingAny.showLoadingModal) {
881
+ onboardingAny.showLoadingModal();
882
+ }
883
+ }
884
+ twitterCallbackHandledRef.current = true;
885
+ try {
886
+ const user = await walletInstance.handleTwitterCallback();
887
+ if (user) {
888
+ setUser(user);
889
+ window.history.replaceState({}, document.title, window.location.pathname);
890
+ // Hide loading modal and close modal on success
891
+ if (onboardingRef.current) {
892
+ const onboardingAny = onboardingRef.current;
893
+ if (onboardingAny.hideLoadingModal) {
894
+ onboardingAny.hideLoadingModal();
895
+ }
896
+ onboardingRef.current.close();
897
+ }
898
+ }
899
+ }
900
+ catch (err) {
901
+ console.error("Twitter callback error:", err);
902
+ // Hide loading modal on error (including whoami API failures)
903
+ if (onboardingRef.current) {
904
+ const onboardingAny = onboardingRef.current;
905
+ // Always try to hide loading modal first
906
+ if (onboardingAny.hideLoadingModal) {
907
+ onboardingAny.hideLoadingModal();
908
+ }
909
+ // Also hide any main modal overlay
910
+ if (onboardingAny.modalOverlay) {
911
+ onboardingAny.modalOverlay.style.display = "none";
912
+ }
913
+ // Show error modal/screen
914
+ const errorMessage = err instanceof Error
915
+ ? err.message
916
+ : "Twitter authentication failed";
917
+ if (onboardingAny.showError) {
918
+ onboardingAny.showError(errorMessage);
919
+ }
920
+ }
921
+ setError(err instanceof Error ? err : new Error("Twitter callback failed"));
922
+ twitterCallbackHandledRef.current = false;
923
+ // Ensure body scroll is restored
924
+ document.body.style.overflow = "";
925
+ }
926
+ };
927
+ handleGoogleCallback();
928
+ handleDiscordCallback();
929
+ handleTwitterCallback();
930
+ // Helper function to restore connection state
931
+ const restoreConnectionState = async (wallet) => {
932
+ try {
933
+ // Verify whoami exists before setting connected
934
+ const whoamiInfo = await wallet.getWhoami();
935
+ if (!whoamiInfo) {
936
+ // If whoami is not available, don't set connected
937
+ return;
938
+ }
939
+ setIsConnected(true);
940
+ const userInfo = await wallet.getUserInfo();
941
+ setUser(userInfo);
942
+ setWhoami(whoamiInfo);
943
+ // Try to get address, but don't fail if not available yet
944
+ try {
945
+ const addr = await wallet.getAddress();
946
+ setAddress(addr);
947
+ }
948
+ catch (addrErr) {
949
+ console.warn("Address not available during restore:", addrErr);
950
+ }
951
+ const cid = await wallet.getChainId();
952
+ setChainId(cid);
953
+ }
954
+ catch (err) {
955
+ console.error("Error restoring connection state:", err);
956
+ // If restore fails, mark as disconnected
957
+ setIsConnected(false);
958
+ }
959
+ };
960
+ // Auto-initialize if enabled
961
+ if (config.autoInit !== false) {
962
+ init().then(() => {
963
+ // After initialization, check if wallet is already connected (from localStorage)
964
+ // The wallet's initialize() method already sets isConnected if authenticated
965
+ // But we need to restore React state if the event was missed
966
+ if (walletInstance && walletInstance.isConnected) {
967
+ // Wallet is already connected, restore state
968
+ restoreConnectionState(walletInstance);
969
+ }
970
+ });
971
+ }
972
+ return () => {
973
+ if (onboardingRef.current) {
974
+ onboardingRef.current.destroy();
975
+ }
976
+ };
977
+ }, []); // Only run once on mount
978
+ // Ref to store the root instance to prevent re-mounting (defined early)
979
+ const externalWalletRootRef = useRef(null);
980
+ const externalWalletMountedRef = useRef(false);
981
+ const externalWalletContainerRef = useRef(null);
982
+ // Show onboarding UI (defined first to avoid hoisting issues)
983
+ const showOnboarding = useCallback(() => {
984
+ setError(null); // Clear any previous errors
985
+ if (!onboardingRef.current) {
986
+ console.error("OnboardingUI not initialized - cannot show modal");
987
+ return;
988
+ }
989
+ if (onboardingRef.current) {
990
+ const onboarding = onboardingRef.current;
991
+ // Re-initialize if destroyed
992
+ if (!onboarding.modalOverlay ||
993
+ !document.body.contains(onboarding.modalOverlay)) {
994
+ onboarding.init();
995
+ // Wait for init to complete
996
+ setTimeout(() => {
997
+ if (onboarding.modalOverlay) {
998
+ // Use requestAnimationFrame to prevent blinking
999
+ requestAnimationFrame(() => {
1000
+ onboarding.modalOverlay.classList.remove("onboarding-modal-closing");
1001
+ onboarding.modalOverlay.classList.add("onboarding-modal-open");
1002
+ onboarding.modalOverlay.style.display = "flex";
1003
+ document.body.style.overflow = "hidden";
1004
+ // Ensure external wallet container is visible when modal opens
1005
+ if (externalWalletsEnabledRef.current &&
1006
+ onboarding.externalWalletContainer) {
1007
+ // Check if we're on OTP screen - if so, don't show external wallets
1008
+ // Check if OTP screen exists AND is actually visible in the DOM
1009
+ const isOtpScreen = onboarding.otpVerificationScreen &&
1010
+ onboarding.otpVerificationScreen.parentElement &&
1011
+ onboarding.otpVerificationScreen.offsetParent !== null;
1012
+ if (!isOtpScreen) {
1013
+ // Only show external wallets if not on OTP screen
1014
+ onboarding.externalWalletContainer.style.display = "";
1015
+ // Show divider only if both email/Google AND external wallets are visible
1016
+ const authMethods = onboarding.config?.authMethods || [
1017
+ "otp",
1018
+ "google",
1019
+ ];
1020
+ const showEmail = authMethods.includes("otp");
1021
+ const showGoogle = authMethods.includes("google");
1022
+ const hasEmailOrGoogle = showEmail || showGoogle;
1023
+ if (onboarding.externalWalletDivider && hasEmailOrGoogle) {
1024
+ onboarding.externalWalletDivider.style.display = "";
1025
+ }
1026
+ else if (onboarding.externalWalletDivider) {
1027
+ onboarding.externalWalletDivider.style.display = "none";
1028
+ }
1029
+ }
1030
+ else {
1031
+ // On OTP screen - hide external wallets
1032
+ onboarding.externalWalletContainer.style.display = "none";
1033
+ if (onboarding.externalWalletDivider) {
1034
+ onboarding.externalWalletDivider.style.display = "none";
1035
+ }
1036
+ }
1037
+ // Always re-render external wallets when modal opens immediately
1038
+ setTimeout(() => {
1039
+ const container = onboarding.externalWalletContainer;
1040
+ if (!container) {
1041
+ return;
1042
+ }
1043
+ // Use the reusable mount function
1044
+ mountExternalWalletsToContainer(container, onboarding);
1045
+ }, 50); // Reduced from 200ms to 50ms
1046
+ }
1047
+ });
1048
+ }
1049
+ }, 50);
1050
+ }
1051
+ else {
1052
+ // Modal already exists, just show it
1053
+ // Use requestAnimationFrame to prevent blinking
1054
+ requestAnimationFrame(() => {
1055
+ // Check if we're on OTP screen - if so, ensure social buttons stay hidden
1056
+ const isOtpScreen = onboarding.otpVerificationScreen &&
1057
+ onboarding.otpVerificationScreen.parentElement &&
1058
+ onboarding.otpVerificationScreen.offsetParent !== null;
1059
+ onboarding.modalOverlay.classList.remove("onboarding-modal-closing");
1060
+ onboarding.modalOverlay.classList.add("onboarding-modal-open");
1061
+ onboarding.modalOverlay.style.display = "flex";
1062
+ if (onboarding.rootElement) {
1063
+ onboarding.rootElement.style.display = "";
1064
+ }
1065
+ document.body.style.overflow = "hidden";
1066
+ // If on OTP screen, ensure all login elements (social buttons, etc.) are hidden
1067
+ if (isOtpScreen) {
1068
+ // On OTP screen - ensure all login elements are hidden
1069
+ if (onboarding.hideLoginElements &&
1070
+ typeof onboarding.hideLoginElements === "function") {
1071
+ onboarding.hideLoginElements();
1072
+ }
1073
+ else {
1074
+ // Fallback: manually hide elements
1075
+ if (onboarding.googleButton)
1076
+ onboarding.googleButton.style.display = "none";
1077
+ if (onboarding.twitterButton)
1078
+ onboarding.twitterButton.style.display = "none";
1079
+ if (onboarding.discordButton)
1080
+ onboarding.discordButton.style.display = "none";
1081
+ if (onboarding.socialGrid)
1082
+ onboarding.socialGrid.style.display = "none";
1083
+ if (onboarding.divider)
1084
+ onboarding.divider.style.display = "none";
1085
+ if (onboarding.passkeyLoginButton)
1086
+ onboarding.passkeyLoginButton.style.display = "none";
1087
+ if (onboarding.passkeySignupLink)
1088
+ onboarding.passkeySignupLink.style.display = "none";
1089
+ if (onboarding.passkeyDivider)
1090
+ onboarding.passkeyDivider.style.display = "none";
1091
+ if (onboarding.externalWalletContainer)
1092
+ onboarding.externalWalletContainer.style.display = "none";
1093
+ if (onboarding.externalWalletDivider)
1094
+ onboarding.externalWalletDivider.style.display = "none";
1095
+ }
1096
+ }
1097
+ // CRITICAL: Always ensure external wallets are mounted when modal reopens
1098
+ if (externalWalletsEnabledRef.current &&
1099
+ onboarding.externalWalletContainer) {
1100
+ // Check if we're on OTP screen - if so, don't show external wallets
1101
+ if (!isOtpScreen) {
1102
+ // Only show external wallets if not on OTP screen
1103
+ onboarding.externalWalletContainer.style.display = "";
1104
+ // Show divider only if both email/Google AND external wallets are visible
1105
+ const authMethods = onboarding.config?.authMethods || [
1106
+ "otp",
1107
+ "google",
1108
+ ];
1109
+ const showEmail = authMethods.includes("otp");
1110
+ const showGoogle = authMethods.includes("google");
1111
+ const hasEmailOrGoogle = showEmail || showGoogle;
1112
+ if (onboarding.externalWalletDivider && hasEmailOrGoogle) {
1113
+ onboarding.externalWalletDivider.style.display = "";
1114
+ }
1115
+ else if (onboarding.externalWalletDivider) {
1116
+ onboarding.externalWalletDivider.style.display = "none";
1117
+ }
1118
+ }
1119
+ else {
1120
+ // On OTP screen - hide external wallets
1121
+ onboarding.externalWalletContainer.style.display = "none";
1122
+ if (onboarding.externalWalletDivider) {
1123
+ onboarding.externalWalletDivider.style.display = "none";
1124
+ }
1125
+ }
1126
+ // Always re-render external wallets when modal opens (even if already mounted)
1127
+ // This ensures they're visible after modal close/reopen
1128
+ setTimeout(() => {
1129
+ const container = onboarding.externalWalletContainer;
1130
+ if (!container) {
1131
+ return;
1132
+ }
1133
+ // Use the reusable mount function (will be defined later, but accessible via closure)
1134
+ if (typeof mountExternalWalletsToContainer === "function") {
1135
+ mountExternalWalletsToContainer(container, onboarding);
1136
+ }
1137
+ }, 50); // Reduced delay for faster initial render
1138
+ }
1139
+ });
1140
+ }
1141
+ }
1142
+ // Note: externalWalletsEnabled is accessed via ref to keep this callback stable
1143
+ }, []);
1144
+ // Initialize IndexedDB
1145
+ const init = useCallback(async () => {
1146
+ if (!walletRef.current || isInitialized)
1147
+ return;
1148
+ setLoading(true);
1149
+ setError(null);
1150
+ try {
1151
+ const authManager = walletRef.current.getAuthManager();
1152
+ const turnkeyService = authManager.turnkeyService;
1153
+ await turnkeyService.init();
1154
+ setIsInitialized(true);
1155
+ // Try to connect wallet to check if user is already authenticated (from localStorage)
1156
+ // This will trigger the wallet's initialize() which checks localStorage
1157
+ try {
1158
+ await walletRef.current.connect();
1159
+ // If connect succeeds, the 'connect' event will be emitted
1160
+ // But also manually restore state in case event was missed
1161
+ if (walletRef.current && walletRef.current.isConnected) {
1162
+ const addr = await walletRef.current.getAddress();
1163
+ const cid = await walletRef.current.getChainId();
1164
+ const userInfo = await walletRef.current.getUserInfo();
1165
+ const whoamiInfo = await walletRef.current.getWhoami();
1166
+ setAddress(addr);
1167
+ setChainId(cid);
1168
+ setUser(userInfo);
1169
+ setWhoami(whoamiInfo);
1170
+ setIsConnected(true);
1171
+ // Restore connection type from localStorage
1172
+ try {
1173
+ const storedType = localStorage.getItem("abstraxn_connection_type");
1174
+ if (storedType &&
1175
+ (storedType === "google" ||
1176
+ storedType === "email" ||
1177
+ storedType === "discord" ||
1178
+ storedType === "x" ||
1179
+ storedType === "passkey")) {
1180
+ setConnectionType(storedType);
1181
+ }
1182
+ }
1183
+ catch (e) {
1184
+ // Ignore localStorage errors
1185
+ }
1186
+ }
1187
+ }
1188
+ catch (err) {
1189
+ // If connect fails (user not authenticated), that's fine
1190
+ // Just don't set connected state
1191
+ if (walletRef.current && walletRef.current.isConnected) {
1192
+ // Wallet says it's connected, restore state
1193
+ try {
1194
+ // Verify whoami exists before setting connected
1195
+ const whoamiInfo = await walletRef.current.getWhoami();
1196
+ if (!whoamiInfo) {
1197
+ // If whoami is not available, don't set connected
1198
+ return;
1199
+ }
1200
+ setIsConnected(true);
1201
+ const userInfo = await walletRef.current.getUserInfo();
1202
+ setUser(userInfo);
1203
+ setWhoami(whoamiInfo);
1204
+ // Try to get address, but don't fail if not available yet
1205
+ try {
1206
+ const addr = await walletRef.current.getAddress();
1207
+ setAddress(addr);
1208
+ }
1209
+ catch (addrErr) {
1210
+ console.warn("Address not available during restore:", addrErr);
1211
+ }
1212
+ const cid = await walletRef.current.getChainId();
1213
+ setChainId(cid);
1214
+ // Restore connection type from localStorage
1215
+ try {
1216
+ const storedType = localStorage.getItem("abstraxn_connection_type");
1217
+ if (storedType &&
1218
+ (storedType === "google" ||
1219
+ storedType === "email" ||
1220
+ storedType === "discord" ||
1221
+ storedType === "x" ||
1222
+ storedType === "passkey")) {
1223
+ setConnectionType(storedType);
1224
+ }
1225
+ }
1226
+ catch (e) {
1227
+ // Ignore localStorage errors
1228
+ }
1229
+ }
1230
+ catch (restoreErr) {
1231
+ console.error("Error restoring connection state:", restoreErr);
1232
+ setIsConnected(false);
1233
+ }
1234
+ }
1235
+ }
1236
+ }
1237
+ catch (err) {
1238
+ const error = err instanceof Error ? err : new Error("Failed to initialize");
1239
+ setError(error);
1240
+ console.error("Init error:", err);
1241
+ }
1242
+ finally {
1243
+ setLoading(false);
1244
+ }
1245
+ }, [isInitialized]);
1246
+ // Connect wallet
1247
+ const connect = useCallback(async () => {
1248
+ if (!walletRef.current)
1249
+ return;
1250
+ setLoading(true);
1251
+ setError(null);
1252
+ try {
1253
+ await walletRef.current.connect();
1254
+ setIsConnected(walletRef.current.isConnected);
1255
+ // Load whoami after connect
1256
+ try {
1257
+ const whoamiInfo = await walletRef.current.getWhoami();
1258
+ setWhoami(whoamiInfo);
1259
+ const addr = await walletRef.current.getAddress();
1260
+ setAddress(addr);
1261
+ }
1262
+ catch (err) {
1263
+ console.error("Error loading whoami after connect:", err);
1264
+ }
1265
+ }
1266
+ catch (err) {
1267
+ if (err instanceof AuthenticationError) {
1268
+ // Show onboarding UI
1269
+ showOnboarding();
1270
+ setError(err);
1271
+ }
1272
+ else {
1273
+ setError(err instanceof Error ? err : new Error("Failed to connect"));
1274
+ }
1275
+ }
1276
+ finally {
1277
+ setLoading(false);
1278
+ }
1279
+ }, [showOnboarding]);
1280
+ // Disconnect wallet
1281
+ const disconnect = useCallback(async () => {
1282
+ setLoading(true);
1283
+ setError(null);
1284
+ disconnectingRef.current = true; // Set flag to prevent useEffect from interfering
1285
+ try {
1286
+ const wasExternalWalletConnected = isExternalWalletConnected;
1287
+ const wasAbstraxnWalletConnected = walletRef.current?.isConnected ?? false;
1288
+ // If external wallet is connected, disconnect it first
1289
+ if (wasExternalWalletConnected &&
1290
+ externalWalletsEnabled &&
1291
+ wagmiDisconnect) {
1292
+ try {
1293
+ await wagmiDisconnect.disconnect();
1294
+ // Wait a bit to ensure wagmi state updates
1295
+ await new Promise((resolve) => setTimeout(resolve, 150));
1296
+ // Reset external wallet state immediately
1297
+ setIsExternalWalletConnected(false);
1298
+ setExternalWalletAddress(null);
1299
+ setExternalWalletChainId(null);
1300
+ setConnectionType(null);
1301
+ // Clear from localStorage
1302
+ try {
1303
+ localStorage.removeItem("abstraxn_connection_type");
1304
+ }
1305
+ catch (e) {
1306
+ // Ignore localStorage errors
1307
+ }
1308
+ explicitConnectionRef.current = false;
1309
+ lastConnectionTimeRef.current = 0;
1310
+ // Always reset main isConnected when disconnecting external wallet
1311
+ // Since the context value is computed as isExternalWalletConnected || isConnected,
1312
+ // we need to set isConnected to false so the overall isConnected becomes false
1313
+ setIsConnected(false);
1314
+ setAddress(null);
1315
+ setChainId(null);
1316
+ }
1317
+ catch (err) {
1318
+ throw err;
1319
+ }
1320
+ }
1321
+ // Disconnect Abstraxn wallet if it's connected
1322
+ if (wasAbstraxnWalletConnected && walletRef.current) {
1323
+ // console.log('Disconnecting Abstraxn wallet...');
1324
+ try {
1325
+ await walletRef.current.disconnect();
1326
+ // After disconnecting Abstraxn wallet, always reset main state
1327
+ // The context value will be computed correctly based on both wallet states
1328
+ setIsConnected(false);
1329
+ setAddress(null);
1330
+ setChainId(null);
1331
+ setConnectionType(null);
1332
+ // Clear from localStorage
1333
+ try {
1334
+ localStorage.removeItem("abstraxn_connection_type");
1335
+ }
1336
+ catch (e) {
1337
+ // Ignore localStorage errors
1338
+ }
1339
+ // console.log('✅ Abstraxn wallet disconnected, main state reset');
1340
+ }
1341
+ catch (err) {
1342
+ console.error("Error disconnecting Abstraxn wallet:", err);
1343
+ // Continue even if Abstraxn wallet disconnect fails, but still reset state
1344
+ setIsConnected(false);
1345
+ setAddress(null);
1346
+ setChainId(null);
1347
+ setConnectionType(null);
1348
+ // Clear from localStorage
1349
+ try {
1350
+ localStorage.removeItem("abstraxn_connection_type");
1351
+ }
1352
+ catch (e) {
1353
+ // Ignore localStorage errors
1354
+ }
1355
+ }
1356
+ }
1357
+ // Final check: if neither wallet is connected, ensure main state is reset
1358
+ // This is a safety check in case both were disconnected
1359
+ const finalExternalConnected = isExternalWalletConnected;
1360
+ const finalAbstraxnConnected = walletRef.current?.isConnected ?? false;
1361
+ if (!finalExternalConnected && !finalAbstraxnConnected) {
1362
+ setIsConnected(false);
1363
+ setAddress(null);
1364
+ setChainId(null);
1365
+ setConnectionType(null);
1366
+ // Clear from localStorage
1367
+ try {
1368
+ localStorage.removeItem("abstraxn_connection_type");
1369
+ }
1370
+ catch (e) {
1371
+ // Ignore localStorage errors
1372
+ }
1373
+ // console.log('✅ Final check: All wallets disconnected, main state reset');
1374
+ }
1375
+ // Reset OTP screen and show initial login form
1376
+ if (onboardingRef.current) {
1377
+ const onboarding = onboardingRef.current;
1378
+ onboarding.resetToLoginForm();
1379
+ // If modal is visible (showing OTP screen), ensure it shows the login form
1380
+ if (onboarding.modalOverlay &&
1381
+ onboarding.modalOverlay.style.display !== "none") {
1382
+ // Modal is visible, just reset the form (already done above)
1383
+ // Ensure modal is in open state
1384
+ onboarding.modalOverlay.classList.remove("onboarding-modal-closing");
1385
+ onboarding.modalOverlay.classList.add("onboarding-modal-open");
1386
+ }
1387
+ }
1388
+ }
1389
+ catch (err) {
1390
+ setError(err instanceof Error ? err : new Error("Failed to disconnect"));
1391
+ throw err;
1392
+ }
1393
+ finally {
1394
+ // Clear disconnecting flag after a short delay to allow state to settle
1395
+ setTimeout(() => {
1396
+ disconnectingRef.current = false;
1397
+ }, 200);
1398
+ setLoading(false);
1399
+ }
1400
+ }, [isExternalWalletConnected, externalWalletsEnabled, wagmiDisconnect]);
1401
+ // Hide onboarding UI
1402
+ const hideOnboarding = useCallback(() => {
1403
+ setError(null); // Clear any previous errors
1404
+ if (onboardingRef.current) {
1405
+ const onboarding = onboardingRef.current;
1406
+ if (onboarding.modalOverlay) {
1407
+ onboarding.modalOverlay.classList.remove("onboarding-modal-open");
1408
+ onboarding.modalOverlay.classList.add("onboarding-modal-closing");
1409
+ setTimeout(() => {
1410
+ if (onboarding.modalOverlay) {
1411
+ onboarding.modalOverlay.style.display = "none";
1412
+ }
1413
+ }, 200);
1414
+ }
1415
+ document.body.style.overflow = "";
1416
+ }
1417
+ }, []);
1418
+ // Get address - works for both Abstraxn wallet and external wallets
1419
+ const getAddress = useCallback(async () => {
1420
+ setError(null); // Clear any previous errors
1421
+ // If external wallet is connected, return external wallet address
1422
+ if (isExternalWalletConnected && externalWalletAddress) {
1423
+ return externalWalletAddress;
1424
+ }
1425
+ // Otherwise use Abstraxn wallet
1426
+ if (!walletRef.current)
1427
+ throw new Error("Wallet not initialized");
1428
+ return await walletRef.current.getAddress();
1429
+ }, [isExternalWalletConnected, externalWalletAddress]);
1430
+ // Get chain ID - works for both Abstraxn wallet and external wallets
1431
+ const getChainId = useCallback(async () => {
1432
+ setError(null); // Clear any previous errors
1433
+ // If external wallet is connected, return external wallet chain ID
1434
+ if (isExternalWalletConnected && externalWalletChainId) {
1435
+ return externalWalletChainId;
1436
+ }
1437
+ // Otherwise use Abstraxn wallet
1438
+ if (!walletRef.current)
1439
+ throw new Error("Wallet not initialized");
1440
+ return await walletRef.current.getChainId();
1441
+ }, [isExternalWalletConnected, externalWalletChainId]);
1442
+ // Switch chain - works for both Abstraxn wallet and external wallets
1443
+ const switchChain = useCallback(async (newChainId) => {
1444
+ setError(null);
1445
+ // If external wallet is connected, use wagmi
1446
+ if (isExternalWalletConnected && wagmiSwitchChain) {
1447
+ try {
1448
+ setLoading(true);
1449
+ await wagmiSwitchChain.switchChainAsync({ chainId: newChainId });
1450
+ }
1451
+ catch (err) {
1452
+ const error = err instanceof Error ? err : new Error("Failed to switch chain");
1453
+ setError(error);
1454
+ throw error;
1455
+ }
1456
+ finally {
1457
+ setLoading(false);
1458
+ }
1459
+ }
1460
+ else {
1461
+ // Otherwise use Abstraxn wallet
1462
+ if (!walletRef.current)
1463
+ throw new Error("Wallet not initialized");
1464
+ try {
1465
+ await walletRef.current.switchChain(newChainId);
1466
+ }
1467
+ catch (err) {
1468
+ setError(err instanceof Error ? err : new Error("Failed to switch chain"));
1469
+ throw err;
1470
+ }
1471
+ }
1472
+ }, [isExternalWalletConnected, wagmiSwitchChain]);
1473
+ // Sign message - works for both Abstraxn wallet and external wallets
1474
+ const signMessage = useCallback(async (message) => {
1475
+ setError(null);
1476
+ // If external wallet is connected, use wagmi
1477
+ if (isExternalWalletConnected &&
1478
+ externalWalletAddress &&
1479
+ wagmiSignMessage) {
1480
+ try {
1481
+ setLoading(true);
1482
+ const signature = await wagmiSignMessage.signMessageAsync({
1483
+ message,
1484
+ });
1485
+ return signature;
1486
+ }
1487
+ catch (err) {
1488
+ const error = err instanceof Error ? err : new Error("Failed to sign message");
1489
+ setError(error);
1490
+ throw error;
1491
+ }
1492
+ finally {
1493
+ setLoading(false);
1494
+ }
1495
+ }
1496
+ // Otherwise use Abstraxn wallet
1497
+ if (!walletRef.current)
1498
+ throw new Error("Wallet not initialized");
1499
+ try {
1500
+ return await walletRef.current.signMessage(message);
1501
+ }
1502
+ catch (err) {
1503
+ setError(err instanceof Error ? err : new Error("Failed to sign message"));
1504
+ throw err;
1505
+ }
1506
+ }, [isExternalWalletConnected, externalWalletAddress, wagmiSignMessage]);
1507
+ // Sign transaction
1508
+ const signTransaction = useCallback(async (tx) => {
1509
+ setError(null); // Clear any previous errors
1510
+ if (!walletRef.current)
1511
+ throw new Error("Wallet not initialized");
1512
+ try {
1513
+ return await walletRef.current.signTransaction(tx);
1514
+ }
1515
+ catch (err) {
1516
+ setError(err instanceof Error ? err : new Error("Failed to sign transaction"));
1517
+ throw err;
1518
+ }
1519
+ }, []);
1520
+ // Send transaction - works for both Abstraxn wallet and external wallets
1521
+ const sendTransaction = useCallback(async (tx) => {
1522
+ setError(null);
1523
+ // If external wallet is connected, use wagmi
1524
+ if (isExternalWalletConnected &&
1525
+ externalWalletAddress &&
1526
+ wagmiSendTransaction) {
1527
+ try {
1528
+ setLoading(true);
1529
+ // Convert TransactionRequest to wagmi format
1530
+ const wagmiTx = {
1531
+ to: tx.to,
1532
+ };
1533
+ if (tx.value) {
1534
+ wagmiTx.value =
1535
+ typeof tx.value === "string"
1536
+ ? parseEther(tx.value)
1537
+ : BigInt(tx.value);
1538
+ }
1539
+ if (tx.data) {
1540
+ wagmiTx.data = tx.data;
1541
+ }
1542
+ if (tx.gasLimit) {
1543
+ wagmiTx.gas = tx.gasLimit;
1544
+ }
1545
+ if (tx.gasPrice) {
1546
+ wagmiTx.gasPrice = tx.gasPrice;
1547
+ }
1548
+ if (tx.maxFeePerGas) {
1549
+ wagmiTx.maxFeePerGas = tx.maxFeePerGas;
1550
+ }
1551
+ if (tx.maxPriorityFeePerGas) {
1552
+ wagmiTx.maxPriorityFeePerGas = tx.maxPriorityFeePerGas;
1553
+ }
1554
+ const hash = await wagmiSendTransaction.sendTransactionAsync(wagmiTx);
1555
+ return {
1556
+ hash: hash,
1557
+ };
1558
+ }
1559
+ catch (err) {
1560
+ const error = err instanceof Error
1561
+ ? err
1562
+ : new Error("Failed to send transaction");
1563
+ setError(error);
1564
+ throw error;
1565
+ }
1566
+ finally {
1567
+ setLoading(false);
1568
+ }
1569
+ }
1570
+ // Otherwise use Abstraxn wallet
1571
+ if (!walletRef.current)
1572
+ throw new Error("Wallet not initialized");
1573
+ try {
1574
+ return await walletRef.current.sendTransaction(tx);
1575
+ }
1576
+ catch (err) {
1577
+ setError(err instanceof Error ? err : new Error("Failed to send transaction"));
1578
+ throw err;
1579
+ }
1580
+ }, [isExternalWalletConnected, externalWalletAddress, wagmiSendTransaction]);
1581
+ // Sign transaction via API (returns signed transaction)
1582
+ const signTransactionViaAPI = useCallback(async (unsignedTransaction, fromAddress) => {
1583
+ if (!walletRef.current)
1584
+ throw new Error("Wallet not initialized");
1585
+ setLoading(true);
1586
+ setError(null);
1587
+ try {
1588
+ return await walletRef.current.signTransactionViaAPI(unsignedTransaction, fromAddress);
1589
+ }
1590
+ catch (err) {
1591
+ const error = err instanceof Error ? err : new Error("Failed to sign transaction");
1592
+ setError(error);
1593
+ throw error;
1594
+ }
1595
+ finally {
1596
+ setLoading(false);
1597
+ }
1598
+ }, []);
1599
+ // Sign and send transaction using backend API
1600
+ const signAndSendTransaction = useCallback(async (unsignedTransaction, fromAddress, rpcUrl) => {
1601
+ if (!walletRef.current)
1602
+ throw new Error("Wallet not initialized");
1603
+ setLoading(true);
1604
+ setError(null);
1605
+ try {
1606
+ return await walletRef.current.signAndSendTransaction(unsignedTransaction, fromAddress, rpcUrl);
1607
+ }
1608
+ catch (err) {
1609
+ const error = err instanceof Error
1610
+ ? err
1611
+ : new Error("Failed to sign and send transaction");
1612
+ setError(error);
1613
+ throw error;
1614
+ }
1615
+ finally {
1616
+ setLoading(false);
1617
+ }
1618
+ }, []);
1619
+ // Login with email OTP (initiate OTP)
1620
+ const loginWithOTP = useCallback(async (email) => {
1621
+ if (!walletRef.current)
1622
+ throw new Error("Wallet not initialized");
1623
+ setLoading(true);
1624
+ setError(null);
1625
+ try {
1626
+ const connectionTypeValue = "email";
1627
+ setConnectionType(connectionTypeValue);
1628
+ try {
1629
+ localStorage.setItem("abstraxn_connection_type", connectionTypeValue);
1630
+ }
1631
+ catch (e) {
1632
+ // Ignore localStorage errors
1633
+ }
1634
+ const result = await walletRef.current.loginWithOTP(email);
1635
+ return result;
1636
+ }
1637
+ catch (err) {
1638
+ const error = err instanceof Error ? err : new Error("Failed to initiate OTP");
1639
+ setError(error);
1640
+ throw error;
1641
+ }
1642
+ finally {
1643
+ setLoading(false);
1644
+ }
1645
+ }, []);
1646
+ // Verify OTP
1647
+ const verifyOTP = useCallback(async (otpId, otpCode) => {
1648
+ if (!walletRef.current)
1649
+ throw new Error("Wallet not initialized");
1650
+ setLoading(true);
1651
+ setError(null);
1652
+ try {
1653
+ // Set connection type for email OTP before verification
1654
+ const connectionTypeValue = "email";
1655
+ setConnectionType(connectionTypeValue);
1656
+ try {
1657
+ localStorage.setItem("abstraxn_connection_type", connectionTypeValue);
1658
+ }
1659
+ catch (e) {
1660
+ // Ignore localStorage errors
1661
+ }
1662
+ const user = await walletRef.current.verifyOTP(otpId, otpCode);
1663
+ setIsConnected(true);
1664
+ setUser(user);
1665
+ setConnectionType("email");
1666
+ try {
1667
+ localStorage.setItem("abstraxn_connection_type", "email");
1668
+ }
1669
+ catch (e) {
1670
+ // Ignore localStorage errors
1671
+ }
1672
+ // Load whoami and address after verification
1673
+ try {
1674
+ const whoamiInfo = await walletRef.current.getWhoami();
1675
+ setWhoami(whoamiInfo);
1676
+ const addr = await walletRef.current.getAddress();
1677
+ setAddress(addr);
1678
+ const cid = await walletRef.current.getChainId();
1679
+ setChainId(cid);
1680
+ }
1681
+ catch (err) {
1682
+ console.error("Error loading whoami after OTP verification:", err);
1683
+ }
1684
+ }
1685
+ catch (err) {
1686
+ const error = err instanceof Error ? err : new Error("Failed to verify OTP");
1687
+ setError(error);
1688
+ throw error;
1689
+ }
1690
+ finally {
1691
+ setLoading(false);
1692
+ }
1693
+ }, []);
1694
+ // Login with Google
1695
+ const loginWithGoogle = useCallback(async () => {
1696
+ if (!walletRef.current)
1697
+ throw new Error("Wallet not initialized");
1698
+ setLoading(true);
1699
+ setError(null);
1700
+ try {
1701
+ const connectionTypeValue = "google";
1702
+ setConnectionType(connectionTypeValue);
1703
+ try {
1704
+ localStorage.setItem("abstraxn_connection_type", connectionTypeValue);
1705
+ }
1706
+ catch (e) {
1707
+ // Ignore localStorage errors
1708
+ }
1709
+ await walletRef.current.loginWithGoogle();
1710
+ // Note: This will redirect, so loading state will be reset on callback
1711
+ }
1712
+ catch (err) {
1713
+ const error = err instanceof Error
1714
+ ? err
1715
+ : new Error("Failed to initiate Google login");
1716
+ setError(error);
1717
+ setLoading(false);
1718
+ throw error;
1719
+ }
1720
+ }, []);
1721
+ // Handle Google callback
1722
+ const handleGoogleCallback = useCallback(async (_force = false) => {
1723
+ if (!walletRef.current)
1724
+ return;
1725
+ setLoading(true);
1726
+ setError(null);
1727
+ try {
1728
+ // handleGoogleCallback() will initialize storage internally
1729
+ // Don't call connect() here as it checks for authentication which isn't available yet
1730
+ const user = await walletRef.current.handleGoogleCallback();
1731
+ if (user) {
1732
+ setUser(user);
1733
+ // Restore connection type for Google OAuth
1734
+ try {
1735
+ const storedType = localStorage.getItem("abstraxn_connection_type");
1736
+ if (storedType === "google") {
1737
+ setConnectionType("google");
1738
+ }
1739
+ }
1740
+ catch (e) {
1741
+ // Ignore localStorage errors
1742
+ }
1743
+ // handleGoogleCallback() already calls whoami and emits 'connect' event
1744
+ // The 'connect' event handler will update whoami, address, and chainId
1745
+ // So we don't need to do it here to avoid duplicate whoami calls
1746
+ }
1747
+ }
1748
+ catch (err) {
1749
+ const error = err instanceof Error
1750
+ ? err
1751
+ : new Error("Failed to handle Google callback");
1752
+ setError(error);
1753
+ throw error;
1754
+ }
1755
+ finally {
1756
+ setLoading(false);
1757
+ }
1758
+ }, []);
1759
+ // Refresh whoami - makes a fresh API call and updates state
1760
+ // For external wallets, whoami is not applicable (no API backend)
1761
+ const refreshWhoami = useCallback(async () => {
1762
+ // If external wallet is connected, return null (whoami is only for Abstraxn wallet)
1763
+ if (isExternalWalletConnected) {
1764
+ return null;
1765
+ }
1766
+ if (!walletRef.current) {
1767
+ return null;
1768
+ }
1769
+ // Check if Abstraxn wallet is actually connected before calling refreshWhoami
1770
+ if (!walletRef.current.isConnected) {
1771
+ return null;
1772
+ }
1773
+ setLoading(true);
1774
+ setError(null);
1775
+ try {
1776
+ const whoamiInfo = await walletRef.current.refreshWhoami();
1777
+ if (whoamiInfo) {
1778
+ setWhoami(whoamiInfo);
1779
+ // Update address if available in whoami response
1780
+ if (whoamiInfo.address) {
1781
+ setAddress(whoamiInfo.address);
1782
+ }
1783
+ }
1784
+ return whoamiInfo;
1785
+ }
1786
+ catch (err) {
1787
+ const error = err instanceof Error ? err : new Error("Failed to refresh whoami");
1788
+ setError(error);
1789
+ throw error;
1790
+ }
1791
+ finally {
1792
+ setLoading(false);
1793
+ }
1794
+ }, [isExternalWalletConnected]);
1795
+ // Sync external wallet state from wagmi
1796
+ useEffect(() => {
1797
+ let checkAddressTimeout = null;
1798
+ // Prevent infinite loops - if we're already updating, skip
1799
+ if (isUpdatingRef.current) {
1800
+ return;
1801
+ }
1802
+ if (!externalWalletsEnabled || !wagmiAccount) {
1803
+ return;
1804
+ }
1805
+ // Prevent auto-connect (but allow restoration from persistence)
1806
+ // Only auto-disconnect if:
1807
+ // 1. wagmiAccount is connected
1808
+ // 2. We don't have explicitConnectionRef set (meaning user didn't explicitly connect)
1809
+ // 3. We haven't already handled auto-disconnect
1810
+ // 4. We don't have a stored address (meaning this isn't a restoration from persistence)
1811
+ if (wagmiAccount.isConnected &&
1812
+ !explicitConnectionRef.current &&
1813
+ !autoDisconnectHandledRef.current &&
1814
+ lastAddressRef.current === null) {
1815
+ autoDisconnectHandledRef.current = true;
1816
+ if (wagmiDisconnect) {
1817
+ setTimeout(() => {
1818
+ wagmiDisconnect.disconnect();
1819
+ }, 0);
1820
+ }
1821
+ return;
1822
+ }
1823
+ // If wagmiAccount is not connected and we have explicitConnectionRef set to false,
1824
+ // it means we explicitly disconnected, so don't try to reconnect
1825
+ if (!wagmiAccount.isConnected) {
1826
+ autoDisconnectHandledRef.current = false;
1827
+ // If we're in the middle of disconnecting, don't interfere
1828
+ if (disconnectingRef.current) {
1829
+ return;
1830
+ }
1831
+ // If we explicitly disconnected (explicitConnectionRef is false and we were connected),
1832
+ // ensure state is reset immediately
1833
+ if (!explicitConnectionRef.current && isExternalWalletConnected) {
1834
+ isUpdatingRef.current = true;
1835
+ setIsExternalWalletConnected(false);
1836
+ setExternalWalletAddress(null);
1837
+ setExternalWalletChainId(null);
1838
+ lastAddressRef.current = null;
1839
+ lastChainIdRef.current = null;
1840
+ // Only reset main state if Abstraxn wallet is also not connected
1841
+ if (!walletRef.current?.isConnected) {
1842
+ setIsConnected(false);
1843
+ setAddress(null);
1844
+ setChainId(null);
1845
+ }
1846
+ lastConnectionTimeRef.current = 0;
1847
+ isUpdatingRef.current = false;
1848
+ return;
1849
+ }
1850
+ // If we're not connected but explicitConnectionRef is true, it means we're trying to reconnect
1851
+ // Don't reset state in this case - wait for the connection to complete
1852
+ if (!wagmiAccount.isConnected && explicitConnectionRef.current) {
1853
+ // Connection is in progress, don't interfere
1854
+ return;
1855
+ }
1856
+ }
1857
+ // Check if external wallet is connected
1858
+ if (wagmiAccount.isConnected && wagmiAccount.address) {
1859
+ const walletAddress = wagmiAccount.address;
1860
+ // Ensure address is properly formatted (should be 0x...)
1861
+ const formattedAddress = walletAddress && typeof walletAddress === "string"
1862
+ ? walletAddress.toLowerCase()
1863
+ : walletAddress;
1864
+ // Use wagmiChainIdHook first (more reliable), then fallback to wagmiAccount.chainId
1865
+ const currentChainId = wagmiChainIdHook || wagmiAccount.chainId || null;
1866
+ // Check if address or chainId actually changed to prevent unnecessary updates
1867
+ const addressChanged = lastAddressRef.current !== formattedAddress;
1868
+ const chainIdChanged = lastChainIdRef.current !== currentChainId;
1869
+ // Also check if we need to reconnect (lastAddressRef is null but wagmiAccount is connected)
1870
+ // This handles the case where user disconnects and then reconnects, OR page reload
1871
+ const needsReconnect = lastAddressRef.current === null &&
1872
+ wagmiAccount.isConnected &&
1873
+ formattedAddress;
1874
+ // Only update if something actually changed OR if we need to reconnect
1875
+ if (addressChanged || chainIdChanged || needsReconnect) {
1876
+ isUpdatingRef.current = true;
1877
+ // If this is a restoration (needsReconnect), set explicitConnectionRef to prevent auto-disconnect
1878
+ if (needsReconnect) {
1879
+ explicitConnectionRef.current = true;
1880
+ autoDisconnectHandledRef.current = false;
1881
+ }
1882
+ // Update address if it changed or if we need to reconnect
1883
+ if (addressChanged || needsReconnect) {
1884
+ setIsExternalWalletConnected(true);
1885
+ setExternalWalletAddress(formattedAddress);
1886
+ setIsConnected(true);
1887
+ setAddress(formattedAddress);
1888
+ lastAddressRef.current = formattedAddress;
1889
+ lastConnectionTimeRef.current = Date.now();
1890
+ }
1891
+ // Update chainId if it changed or if we need to reconnect
1892
+ if ((chainIdChanged && currentChainId) ||
1893
+ (needsReconnect && currentChainId)) {
1894
+ setExternalWalletChainId(currentChainId);
1895
+ setChainId(currentChainId);
1896
+ lastChainIdRef.current = currentChainId;
1897
+ }
1898
+ isUpdatingRef.current = false;
1899
+ // Close the onboarding modal after successful external wallet connection (only once)
1900
+ if (addressChanged && onboardingRef.current) {
1901
+ const onboardingAny = onboardingRef.current;
1902
+ if (onboardingAny.modalOverlay) {
1903
+ setTimeout(() => {
1904
+ if (onboardingAny.modalOverlay) {
1905
+ onboardingAny.modalOverlay.classList.remove("onboarding-modal-open");
1906
+ onboardingAny.modalOverlay.classList.add("onboarding-modal-closing");
1907
+ setTimeout(() => {
1908
+ if (onboardingAny.modalOverlay) {
1909
+ onboardingAny.modalOverlay.style.display = "none";
1910
+ }
1911
+ }, 200);
1912
+ document.body.style.overflow = "";
1913
+ }
1914
+ }, 100);
1915
+ }
1916
+ }
1917
+ }
1918
+ }
1919
+ else if (!wagmiAccount.isConnected) {
1920
+ // Only reset state if:
1921
+ // 1. We have a stored address (meaning we were connected), AND
1922
+ // 2. It's been more than 1 second since last connection (to avoid premature reset during re-renders), AND
1923
+ // 3. This is not an auto-disconnect scenario
1924
+ const timeSinceLastConnection = Date.now() - lastConnectionTimeRef.current;
1925
+ const shouldReset = lastAddressRef.current !== null &&
1926
+ timeSinceLastConnection > 1000 &&
1927
+ !autoDisconnectHandledRef.current;
1928
+ if (shouldReset) {
1929
+ isUpdatingRef.current = true;
1930
+ setIsExternalWalletConnected(false);
1931
+ setExternalWalletAddress(null);
1932
+ setExternalWalletChainId(null);
1933
+ lastAddressRef.current = null;
1934
+ lastChainIdRef.current = null;
1935
+ // Only reset if Abstraxn wallet is also not connected
1936
+ if (!walletRef.current?.isConnected) {
1937
+ setIsConnected(false);
1938
+ setAddress(null);
1939
+ setChainId(null);
1940
+ }
1941
+ explicitConnectionRef.current = false;
1942
+ lastConnectionTimeRef.current = 0;
1943
+ isUpdatingRef.current = false;
1944
+ }
1945
+ }
1946
+ // Cleanup function
1947
+ return () => {
1948
+ if (checkAddressTimeout) {
1949
+ clearTimeout(checkAddressTimeout);
1950
+ }
1951
+ };
1952
+ }, [
1953
+ wagmiAccount?.isConnected,
1954
+ wagmiAccount?.address,
1955
+ wagmiAccount?.chainId,
1956
+ wagmiChainIdHook,
1957
+ externalWalletsEnabled,
1958
+ wagmiDisconnect,
1959
+ ]);
1960
+ // Connect external wallet
1961
+ const connectExternalWallet = useCallback(async (connectorId) => {
1962
+ if (!externalWalletsEnabled || !wagmiConnect || !wagmiDisconnect) {
1963
+ const error = new Error("External wallets are not enabled or wagmi is not initialized");
1964
+ throw error;
1965
+ }
1966
+ setLoading(true);
1967
+ setError(null);
1968
+ try {
1969
+ if (wagmiAccount?.isConnected) {
1970
+ try {
1971
+ await wagmiDisconnect.disconnect();
1972
+ await new Promise((resolve) => setTimeout(resolve, 100));
1973
+ }
1974
+ catch (disconnectErr) {
1975
+ // Ignore
1976
+ }
1977
+ }
1978
+ // Try to find connector by exact ID first
1979
+ let connector = wagmiConnect.connectors.find((c) => c.id === connectorId);
1980
+ // If not found, try to find by matching patterns for specific wallets
1981
+ if (!connector) {
1982
+ const connectorIdLower = connectorId.toLowerCase();
1983
+ // Check for MetaMask - prioritize io.metamask over injected
1984
+ if (connectorIdLower.includes("metamask") ||
1985
+ connectorIdLower.includes("io.metamask")) {
1986
+ // First, try to find io.metamask specifically
1987
+ connector = wagmiConnect.connectors.find((c) => {
1988
+ const id = c.id.toLowerCase();
1989
+ return id === "io.metamask" || id.includes("io.metamask");
1990
+ });
1991
+ // If io.metamask not found, fall back to injected (but only if MetaMask is requested)
1992
+ if (!connector) {
1993
+ connector = wagmiConnect.connectors.find((c) => {
1994
+ const id = c.id.toLowerCase();
1995
+ const name = c.name?.toLowerCase() || "";
1996
+ return ((id.includes("injected") || id.includes("metamask")) &&
1997
+ (name.includes("metamask") ||
1998
+ (typeof window !== "undefined" &&
1999
+ window.ethereum?.isMetaMask)));
2000
+ });
2001
+ }
2002
+ }
2003
+ // Check for generic injected (only if not MetaMask)
2004
+ else if (connectorIdLower.includes("injected")) {
2005
+ connector = wagmiConnect.connectors.find((c) => {
2006
+ const id = c.id.toLowerCase();
2007
+ // Exclude io.metamask and other specific connectors
2008
+ return (id === "injected" &&
2009
+ !id.includes("io.metamask") &&
2010
+ !id.includes("com.coinbase") &&
2011
+ !id.includes("app.phantom"));
2012
+ });
2013
+ }
2014
+ // Check for Coinbase Wallet
2015
+ else if (connectorIdLower.includes("coinbase") ||
2016
+ connectorIdLower.includes("com.coinbase.wallet")) {
2017
+ connector = wagmiConnect.connectors.find((c) => {
2018
+ const id = c.id.toLowerCase();
2019
+ const name = c.name?.toLowerCase() || "";
2020
+ return (id.includes("coinbase") ||
2021
+ id.includes("com.coinbase") ||
2022
+ name.includes("coinbase"));
2023
+ });
2024
+ }
2025
+ // Check for Phantom
2026
+ else if (connectorIdLower.includes("phantom") ||
2027
+ connectorIdLower.includes("app.phantom")) {
2028
+ connector = wagmiConnect.connectors.find((c) => {
2029
+ const id = c.id.toLowerCase();
2030
+ const name = c.name?.toLowerCase() || "";
2031
+ return (id.includes("phantom") ||
2032
+ id.includes("app.phantom") ||
2033
+ name.includes("phantom"));
2034
+ });
2035
+ }
2036
+ // Check for WalletConnect
2037
+ else if (connectorIdLower.includes("walletconnect") ||
2038
+ connectorIdLower.includes("wallet_connect")) {
2039
+ connector = wagmiConnect.connectors.find((c) => {
2040
+ const id = c.id.toLowerCase();
2041
+ const name = c.name?.toLowerCase() || "";
2042
+ return (id.includes("walletconnect") ||
2043
+ id.includes("wallet_connect") ||
2044
+ name.includes("walletconnect") ||
2045
+ name.includes("wallet connect"));
2046
+ });
2047
+ }
2048
+ // Try to find any connector that matches (fallback)
2049
+ else {
2050
+ connector = wagmiConnect.connectors.find((c) => {
2051
+ const id = c.id.toLowerCase();
2052
+ return id === connectorIdLower || id.includes(connectorIdLower);
2053
+ });
2054
+ }
2055
+ }
2056
+ if (!connector) {
2057
+ const availableIds = wagmiConnect.connectors
2058
+ .map((c) => c.id)
2059
+ .join(", ");
2060
+ const error = new Error(`Connector "${connectorId}" not found. Available connectors: ${availableIds}`);
2061
+ throw error;
2062
+ }
2063
+ autoDisconnectHandledRef.current = false;
2064
+ explicitConnectionRef.current = true;
2065
+ // CRITICAL: Reset refs before connecting to ensure reconnect is detected
2066
+ // This ensures that when we reconnect after a disconnect, the address change is detected
2067
+ lastAddressRef.current = null;
2068
+ lastChainIdRef.current = null;
2069
+ try {
2070
+ // Use connectAsync to properly await the connection
2071
+ const result = await wagmiConnect.connectAsync({ connector });
2072
+ // Use the result directly to set state immediately
2073
+ // This avoids waiting for the useAccount hook to update, which can be delayed
2074
+ if (result.accounts && result.accounts.length > 0) {
2075
+ const address = result.accounts[0];
2076
+ const chainId = result.chainId;
2077
+ const formattedAddress = address.toLowerCase();
2078
+ // Set state directly
2079
+ setExternalWalletAddress(formattedAddress);
2080
+ setIsExternalWalletConnected(true);
2081
+ setAddress(formattedAddress);
2082
+ setIsConnected(true);
2083
+ if (chainId) {
2084
+ setChainId(chainId);
2085
+ setExternalWalletChainId(chainId);
2086
+ }
2087
+ // Track connection type
2088
+ const connectorId = connector.id.toLowerCase();
2089
+ const connectorName = connector.name?.toLowerCase() || "";
2090
+ let connectionTypeValue;
2091
+ // Map connector to connection type
2092
+ if (connectorId.includes("metamask") ||
2093
+ connectorId.includes("io.metamask") ||
2094
+ connectorName.includes("metamask")) {
2095
+ connectionTypeValue = "metamask";
2096
+ }
2097
+ else if (connectorId.includes("walletconnect") ||
2098
+ connectorId.includes("wallet_connect") ||
2099
+ connectorName.includes("walletconnect")) {
2100
+ connectionTypeValue = "walletconnect";
2101
+ }
2102
+ else if (connectorId.includes("coinbase") ||
2103
+ connectorId.includes("com.coinbase") ||
2104
+ connectorName.includes("coinbase")) {
2105
+ connectionTypeValue = "coinbase";
2106
+ }
2107
+ else if (connectorId.includes("phantom") ||
2108
+ connectorId.includes("app.phantom") ||
2109
+ connectorName.includes("phantom")) {
2110
+ connectionTypeValue = "phantom";
2111
+ }
2112
+ else if (connectorId.includes("injected")) {
2113
+ connectionTypeValue = "injected";
2114
+ }
2115
+ else {
2116
+ connectionTypeValue = connectorName || connectorId;
2117
+ }
2118
+ setConnectionType(connectionTypeValue);
2119
+ try {
2120
+ localStorage.setItem("abstraxn_connection_type", connectionTypeValue);
2121
+ }
2122
+ catch (e) { }
2123
+ // Update refs
2124
+ lastConnectionTimeRef.current = Date.now();
2125
+ lastAddressRef.current = formattedAddress;
2126
+ lastChainIdRef.current = chainId;
2127
+ explicitConnectionRef.current = true;
2128
+ // Close the onboarding modal immediately
2129
+ if (onboardingRef.current) {
2130
+ const onboardingAny = onboardingRef.current;
2131
+ if (onboardingAny.modalOverlay) {
2132
+ setTimeout(() => {
2133
+ if (onboardingAny.modalOverlay) {
2134
+ onboardingAny.modalOverlay.classList.remove("onboarding-modal-open");
2135
+ onboardingAny.modalOverlay.classList.add("onboarding-modal-closing");
2136
+ setTimeout(() => {
2137
+ if (onboardingAny.modalOverlay) {
2138
+ onboardingAny.modalOverlay.style.display = "none";
2139
+ }
2140
+ }, 200);
2141
+ document.body.style.overflow = "";
2142
+ }
2143
+ }, 100);
2144
+ }
2145
+ }
2146
+ }
2147
+ }
2148
+ catch (connectError) {
2149
+ console.error('WalletConnect connection error:', connectError);
2150
+ explicitConnectionRef.current = false;
2151
+ throw connectError;
2152
+ }
2153
+ }
2154
+ catch (err) {
2155
+ explicitConnectionRef.current = false;
2156
+ const error = err instanceof Error
2157
+ ? err
2158
+ : new Error(`Failed to connect external wallet: ${err instanceof Error ? err.message : String(err)}`);
2159
+ setError(error);
2160
+ throw error;
2161
+ }
2162
+ finally {
2163
+ setLoading(false);
2164
+ }
2165
+ }, [
2166
+ externalWalletsEnabled,
2167
+ wagmiConnect,
2168
+ wagmiDisconnect,
2169
+ wagmiAccount?.isConnected,
2170
+ ]);
2171
+ // Disconnect external wallet
2172
+ const disconnectExternalWallet = useCallback(async () => {
2173
+ if (!externalWalletsEnabled || !wagmiDisconnect) {
2174
+ return;
2175
+ }
2176
+ setLoading(true);
2177
+ setError(null);
2178
+ disconnectingRef.current = true; // Set flag to prevent useEffect from interfering
2179
+ try {
2180
+ const wasAbstraxnConnected = walletRef.current?.isConnected ?? false;
2181
+ // Await the disconnect to ensure it completes
2182
+ await wagmiDisconnect.disconnect();
2183
+ // Wait a bit to ensure wagmi state updates
2184
+ await new Promise((resolve) => setTimeout(resolve, 150));
2185
+ // Reset external wallet state immediately
2186
+ setIsExternalWalletConnected(false);
2187
+ setExternalWalletAddress(null);
2188
+ setExternalWalletChainId(null);
2189
+ setConnectionType(null);
2190
+ // Clear from localStorage
2191
+ try {
2192
+ localStorage.removeItem("abstraxn_connection_type");
2193
+ }
2194
+ catch (e) {
2195
+ // Ignore localStorage errors
2196
+ }
2197
+ explicitConnectionRef.current = false;
2198
+ lastConnectionTimeRef.current = 0;
2199
+ // CRITICAL: Reset refs so reconnect is detected as a new connection
2200
+ lastAddressRef.current = null;
2201
+ lastChainIdRef.current = null;
2202
+ // Always reset main state when disconnecting external wallet
2203
+ // The context value is computed as isExternalWalletConnected || isConnected,
2204
+ // so setting both to false ensures the overall isConnected becomes false
2205
+ setIsConnected(false);
2206
+ setAddress(null);
2207
+ setChainId(null);
2208
+ }
2209
+ catch (err) {
2210
+ const error = err instanceof Error
2211
+ ? err
2212
+ : new Error("Failed to disconnect external wallet");
2213
+ console.error("Error disconnecting external wallet:", error);
2214
+ setError(error);
2215
+ throw error;
2216
+ }
2217
+ finally {
2218
+ // Clear disconnecting flag after a short delay to allow state to settle
2219
+ setTimeout(() => {
2220
+ disconnectingRef.current = false;
2221
+ }, 200);
2222
+ setLoading(false);
2223
+ }
2224
+ }, [externalWalletsEnabled, wagmiDisconnect]);
2225
+ // Helper function to get network name from chain ID
2226
+ const getNetworkName = useCallback((chainId) => {
2227
+ const chainNames = {
2228
+ 1: "Ethereum Mainnet",
2229
+ 5: "Goerli",
2230
+ 11155111: "Sepolia",
2231
+ 137: "Polygon",
2232
+ 80001: "Mumbai",
2233
+ 80002: "Polygon Amoy",
2234
+ 8453: "Base",
2235
+ 84531: "Base Goerli",
2236
+ 42161: "Arbitrum One",
2237
+ 421613: "Arbitrum Goerli",
2238
+ 10: "Optimism",
2239
+ 420: "Optimism Goerli",
2240
+ 56: "BNB Smart Chain",
2241
+ 97: "BNB Smart Chain Testnet",
2242
+ };
2243
+ return chainNames[chainId] || `Chain ${chainId}`;
2244
+ }, []);
2245
+ // Switch external wallet chain (dedicated function for external wallets)
2246
+ const switchExternalWalletChain = useCallback(async (chainId) => {
2247
+ if (!externalWalletsEnabled || !wagmiSwitchChain) {
2248
+ const error = new Error("External wallets are not enabled or chain switching is not available");
2249
+ setError(error);
2250
+ throw error;
2251
+ }
2252
+ setError(null);
2253
+ setLoading(true);
2254
+ try {
2255
+ // Use switchChainAsync for wagmi v3
2256
+ await wagmiSwitchChain.switchChainAsync({ chainId });
2257
+ // Update local state - wagmi will update chainId via useChainId hook
2258
+ // But we also update our local state for immediate feedback
2259
+ setExternalWalletChainId(chainId);
2260
+ }
2261
+ catch (err) {
2262
+ const error = err instanceof Error
2263
+ ? err
2264
+ : new Error(`Failed to switch chain: ${err instanceof Error ? err.message : String(err)}`);
2265
+ setError(error);
2266
+ throw error;
2267
+ }
2268
+ finally {
2269
+ setLoading(false);
2270
+ }
2271
+ }, [externalWalletsEnabled, wagmiSwitchChain]);
2272
+ // Get available connectors - return all connectors with their IDs and names
2273
+ // Filter out generic 'injected' connector if 'io.metamask' is available (prefer specific connectors)
2274
+ // The ExternalWalletButtons component will handle additional deduplication and display
2275
+ const availableConnectors = externalWalletsEnabled && wagmiConnect && wagmiConnect.connectors
2276
+ ? wagmiConnect.connectors
2277
+ .filter((connector) => {
2278
+ // If io.metamask exists, exclude generic 'injected' connector
2279
+ const hasIoMetaMask = wagmiConnect.connectors.some((c) => c.id === "io.metamask");
2280
+ if (hasIoMetaMask && connector.id === "injected") {
2281
+ return false; // Exclude generic injected when io.metamask is available
2282
+ }
2283
+ return true;
2284
+ })
2285
+ .map((connector) => ({
2286
+ id: connector.id,
2287
+ name: connector.name,
2288
+ icon: connector.icon, // Pass the icon from the connector
2289
+ }))
2290
+ : [];
2291
+ // Determine the current address (external wallet takes precedence)
2292
+ const currentAddress = isExternalWalletConnected && externalWalletAddress
2293
+ ? externalWalletAddress
2294
+ : address;
2295
+ // Determine the current chain ID (external wallet takes precedence)
2296
+ const currentChainId = isExternalWalletConnected && externalWalletChainId
2297
+ ? externalWalletChainId
2298
+ : chainId;
2299
+ // Fetch balance for Abstraxn wallet (not external wallet)
2300
+ useEffect(() => {
2301
+ if (isExternalWalletConnected || !address || !currentChainId) {
2302
+ setWalletBalance(null);
2303
+ return;
2304
+ }
2305
+ const fetchBalance = async () => {
2306
+ try {
2307
+ const currentChain = getChainById(currentChainId);
2308
+ if (!currentChain || currentChain.type !== "evm") {
2309
+ setWalletBalance(null);
2310
+ return;
2311
+ }
2312
+ const publicClient = createPublicClient({
2313
+ chain: {
2314
+ id: currentChain.id,
2315
+ name: currentChain.name,
2316
+ nativeCurrency: currentChain.nativeCurrency,
2317
+ rpcUrls: {
2318
+ default: {
2319
+ http: [currentChain.rpcUrl],
2320
+ },
2321
+ },
2322
+ },
2323
+ transport: http(currentChain.rpcUrl),
2324
+ });
2325
+ const balance = await publicClient.getBalance({
2326
+ address: address,
2327
+ });
2328
+ setWalletBalance(balance);
2329
+ }
2330
+ catch (error) {
2331
+ console.error("Failed to fetch balance:", error);
2332
+ setWalletBalance(null);
2333
+ }
2334
+ };
2335
+ fetchBalance();
2336
+ // Refetch balance every 10 seconds
2337
+ const interval = setInterval(fetchBalance, 10000);
2338
+ return () => clearInterval(interval);
2339
+ }, [address, currentChainId, isExternalWalletConnected]);
2340
+ // Compute available chains from config - supports both legacy and new format
2341
+ const availableChains = useMemo(() => {
2342
+ const chains = [];
2343
+ const chainConfig = config.chains;
2344
+ // Priority 1: Check new chains config format
2345
+ if (chainConfig?.supportedEvmChains &&
2346
+ chainConfig.supportedEvmChains.length > 0) {
2347
+ // Use configured chains from new format
2348
+ chainConfig.supportedEvmChains.forEach((chainName) => {
2349
+ const chain = EVM_CHAINS[chainName];
2350
+ if (chain) {
2351
+ chains.push(chain);
2352
+ }
2353
+ });
2354
+ }
2355
+ // Priority 2: Check legacy supportedChains format
2356
+ else if (config.supportedChains && config.supportedChains.length > 0) {
2357
+ // Convert legacy Chain format to ChainData
2358
+ config.supportedChains.forEach((legacyChain) => {
2359
+ // Try to find matching chain by ID
2360
+ const chainData = getChainById(legacyChain.id);
2361
+ if (chainData) {
2362
+ chains.push(chainData);
2363
+ }
2364
+ else {
2365
+ // If not found, create ChainData from legacy chain
2366
+ const newChain = {
2367
+ id: legacyChain.id,
2368
+ name: legacyChain.name.toLowerCase().replace(/\s+/g, "-"),
2369
+ displayName: legacyChain.name,
2370
+ rpcUrl: legacyChain.rpcUrl,
2371
+ explorerUrl: `https://etherscan.io`, // Default explorer
2372
+ nativeCurrency: legacyChain.nativeCurrency,
2373
+ type: "evm", // Assume EVM for legacy chains
2374
+ isTestnet: legacyChain.id !== 1 &&
2375
+ legacyChain.id !== 137 &&
2376
+ legacyChain.id !== 8453, // Common mainnets
2377
+ };
2378
+ chains.push(newChain);
2379
+ }
2380
+ });
2381
+ }
2382
+ // Priority 3: Default chains only if nothing configured at all
2383
+ // Only show defaults if both config.chains and config.supportedChains are missing
2384
+ else if (!chainConfig && !config.supportedChains) {
2385
+ chains.push(EVM_CHAINS.ethereum, EVM_CHAINS.polygon, EVM_CHAINS.base);
2386
+ }
2387
+ // Add Solana chains if enabled (from new config format)
2388
+ if (chainConfig?.enableSolana) {
2389
+ chains.push(SOLANA_CHAINS.solana);
2390
+ }
2391
+ // Remove duplicates by chain ID
2392
+ const uniqueChains = chains.filter((chain, index, self) => index === self.findIndex((c) => c.id === chain.id));
2393
+ // Only return defaults if absolutely nothing was configured
2394
+ return uniqueChains.length > 0
2395
+ ? uniqueChains
2396
+ : !chainConfig && !config.supportedChains
2397
+ ? [EVM_CHAINS.ethereum, EVM_CHAINS.polygon, EVM_CHAINS.base]
2398
+ : [];
2399
+ }, [config.chains, config.supportedChains]);
2400
+ // Get current chain data
2401
+ const currentChain = useMemo(() => {
2402
+ if (!currentChainId)
2403
+ return null;
2404
+ return getChainById(currentChainId) || null;
2405
+ }, [currentChainId]);
2406
+ // Enhanced switchChain that works for both EVM and Solana
2407
+ const switchChainEnhanced = useCallback(async (targetChainId) => {
2408
+ if (!walletRef.current) {
2409
+ throw new Error("Wallet not initialized");
2410
+ }
2411
+ // Check if chain is in availableChains (user-configured chains)
2412
+ const targetChainInAvailable = availableChains?.find((c) => c.id === targetChainId);
2413
+ if (!targetChainInAvailable) {
2414
+ throw new Error(`Chain with ID ${targetChainId} is not available. Please configure it in your SDK setup.`);
2415
+ }
2416
+ const targetChain = getChainById(targetChainId);
2417
+ if (!targetChain) {
2418
+ throw new Error(`Chain with ID ${targetChainId} is not supported`);
2419
+ }
2420
+ setLoading(true);
2421
+ setError(null);
2422
+ try {
2423
+ if (targetChain.type === "evm") {
2424
+ // For EVM chains, use the existing switchChain
2425
+ if (isExternalWalletConnected && wagmiSwitchChain) {
2426
+ // External wallet - use wagmi switchChain
2427
+ await wagmiSwitchChain.switchChainAsync({ chainId: targetChainId });
2428
+ setExternalWalletChainId(targetChainId);
2429
+ }
2430
+ else {
2431
+ // Abstraxn wallet - try to switch, but if chain is not in wallet's supportedChains,
2432
+ // we'll update the state directly (for chains that are in availableChains but not in wallet config)
2433
+ try {
2434
+ await switchChain(targetChainId);
2435
+ }
2436
+ catch (switchErr) {
2437
+ // If the wallet doesn't support this chain, but it's in availableChains,
2438
+ // we can still update the state to allow UI to work
2439
+ if (switchErr?.message?.includes("not supported") &&
2440
+ targetChainInAvailable) {
2441
+ // Update chainId state directly for chains in availableChains
2442
+ setChainId(targetChainId);
2443
+ // Emit chainChanged event if wallet supports it
2444
+ if (walletRef.current &&
2445
+ typeof walletRef.current.emit === "function") {
2446
+ walletRef.current.emit("chainChanged", targetChainId);
2447
+ }
2448
+ }
2449
+ else {
2450
+ throw switchErr;
2451
+ }
2452
+ }
2453
+ }
2454
+ }
2455
+ else if (targetChain.type === "solana") {
2456
+ // For Solana, we might need different handling
2457
+ // For now, just update the chainId state
2458
+ // TODO: Implement actual Solana chain switching when wallet supports it
2459
+ setChainId(targetChainId);
2460
+ }
2461
+ // Update chainId state
2462
+ if (!isExternalWalletConnected) {
2463
+ setChainId(targetChainId);
2464
+ }
2465
+ }
2466
+ catch (err) {
2467
+ const error = err instanceof Error
2468
+ ? err
2469
+ : new Error(`Failed to switch to chain ${targetChainId}: ${err instanceof Error ? err.message : String(err)}`);
2470
+ setError(error);
2471
+ throw error;
2472
+ }
2473
+ finally {
2474
+ setLoading(false);
2475
+ }
2476
+ }, [isExternalWalletConnected, wagmiSwitchChain, switchChain, availableChains]);
2477
+ const value = {
2478
+ wallet: walletRef.current,
2479
+ isInitialized,
2480
+ isConnected: isExternalWalletConnected || isConnected, // True if either wallet is connected
2481
+ address: currentAddress, // Use external wallet address if connected, otherwise Abstraxn wallet address
2482
+ user,
2483
+ whoami,
2484
+ chainId: currentChainId, // Use external wallet chain ID if connected, otherwise Abstraxn wallet chain ID
2485
+ error,
2486
+ loading,
2487
+ init,
2488
+ connect,
2489
+ disconnect,
2490
+ showOnboarding,
2491
+ hideOnboarding,
2492
+ getAddress,
2493
+ getChainId,
2494
+ switchChain,
2495
+ signMessage,
2496
+ signTransaction,
2497
+ sendTransaction,
2498
+ signTransactionViaAPI,
2499
+ signAndSendTransaction,
2500
+ loginWithOTP,
2501
+ verifyOTP,
2502
+ loginWithGoogle,
2503
+ handleGoogleCallback: () => handleGoogleCallback(true),
2504
+ refreshWhoami,
2505
+ uiConfig: config.ui,
2506
+ // External wallet methods
2507
+ connectExternalWallet: externalWalletsEnabled
2508
+ ? connectExternalWallet
2509
+ : undefined,
2510
+ disconnectExternalWallet: externalWalletsEnabled
2511
+ ? disconnectExternalWallet
2512
+ : undefined,
2513
+ isExternalWalletConnected: externalWalletsEnabled
2514
+ ? isExternalWalletConnected
2515
+ : undefined,
2516
+ externalWalletAddress: externalWalletsEnabled
2517
+ ? externalWalletAddress
2518
+ : undefined,
2519
+ externalWalletChainId: externalWalletsEnabled
2520
+ ? externalWalletChainId
2521
+ : undefined,
2522
+ externalWalletBalance: externalWalletsEnabled &&
2523
+ wagmiBalance?.data &&
2524
+ typeof wagmiBalance.data === "object" &&
2525
+ wagmiBalance.data !== null &&
2526
+ "value" in wagmiBalance.data
2527
+ ? wagmiBalance.data.value
2528
+ : undefined,
2529
+ externalWalletNetwork: externalWalletsEnabled && wagmiChainIdHook
2530
+ ? getNetworkName(wagmiChainIdHook)
2531
+ : undefined,
2532
+ availableConnectors: externalWalletsEnabled
2533
+ ? availableConnectors
2534
+ : undefined,
2535
+ switchExternalWalletChain: externalWalletsEnabled
2536
+ ? switchExternalWalletChain
2537
+ : undefined,
2538
+ // Chain management
2539
+ switchChainEnhanced,
2540
+ currentChain,
2541
+ availableChains,
2542
+ // Balance (for Abstraxn wallet)
2543
+ walletBalance: !isExternalWalletConnected ? walletBalance : undefined,
2544
+ // Connection type
2545
+ connectionType,
2546
+ };
2547
+ // Ref to store latest value to avoid dependency issues (defined after value)
2548
+ const valueRef = useRef(value);
2549
+ valueRef.current = value;
2550
+ // Debug log when address changes
2551
+ // useEffect(() => {
2552
+ // console.log('🔍 Context value check:', {
2553
+ // currentAddress,
2554
+ // isConnected: isExternalWalletConnected || isConnected,
2555
+ // isExternalWalletConnected,
2556
+ // externalWalletAddress,
2557
+ // address,
2558
+ // explicitConnection: explicitConnectionRef.current,
2559
+ // });
2560
+ // }, [currentAddress, isExternalWalletConnected, isConnected, externalWalletAddress, address]);
2561
+ // Reusable function to mount external wallets (moved here so it can be used by showOnboarding)
2562
+ const mountExternalWalletsToContainer = useCallback(async (container, onboardingAny) => {
2563
+ if (!container) {
2564
+ return;
2565
+ }
2566
+ // Check if we're on OTP screen - if so, don't show external wallets
2567
+ const isOtpScreen = onboardingAny.otpVerificationScreen &&
2568
+ onboardingAny.otpVerificationScreen.parentElement &&
2569
+ onboardingAny.otpVerificationScreen.offsetParent !== null;
2570
+ if (!isOtpScreen) {
2571
+ // Only show external wallets if not on OTP screen
2572
+ container.style.display = "";
2573
+ // Show divider only if both email/Google AND external wallets are visible
2574
+ const authMethods = onboardingAny.config?.authMethods || [
2575
+ "otp",
2576
+ "google",
2577
+ ];
2578
+ const showEmail = authMethods.includes("otp");
2579
+ const showGoogle = authMethods.includes("google");
2580
+ const hasEmailOrGoogle = showEmail || showGoogle;
2581
+ if (onboardingAny.externalWalletDivider && hasEmailOrGoogle) {
2582
+ onboardingAny.externalWalletDivider.style.display = "";
2583
+ }
2584
+ else if (onboardingAny.externalWalletDivider) {
2585
+ onboardingAny.externalWalletDivider.style.display = "none";
2586
+ }
2587
+ }
2588
+ else {
2589
+ // On OTP screen - hide external wallets
2590
+ container.style.display = "none";
2591
+ if (onboardingAny.externalWalletDivider) {
2592
+ onboardingAny.externalWalletDivider.style.display = "none";
2593
+ }
2594
+ return;
2595
+ }
2596
+ // Check if root exists and container is still in DOM
2597
+ const containerInDOM = container.parentElement !== null;
2598
+ // Check if the container is the same as the one we created the root for
2599
+ const isSameContainer = externalWalletContainerRef.current === container;
2600
+ const rootStillValid = externalWalletRootRef.current && containerInDOM && isSameContainer;
2601
+ const renderExternalWalletButtons = () => (_jsx(AbstraxnContext.Provider, { value: valueRef.current, children: _jsx(ExternalWalletButtons, { onConnect: async () => { }, onShowMoreWallets: () => {
2602
+ const onboardingAny = onboardingRef.current;
2603
+ if (onboardingAny?.emailForm) {
2604
+ onboardingAny.emailForm.style.display = "none";
2605
+ }
2606
+ if (onboardingAny?.googleButton) {
2607
+ onboardingAny.googleButton.style.display = "none";
2608
+ }
2609
+ // Hide all passkey elements when wallet selection screen is shown
2610
+ if (onboardingAny?.passkeyLoginButton) {
2611
+ onboardingAny.passkeyLoginButton.style.display = "none";
2612
+ }
2613
+ if (onboardingAny?.passkeySignupLink) {
2614
+ onboardingAny.passkeySignupLink.style.display = "none";
2615
+ }
2616
+ if (onboardingAny?.passkeyErrorElement) {
2617
+ onboardingAny.passkeyErrorElement.style.display = "none";
2618
+ }
2619
+ if (onboardingAny?.passkeyDivider) {
2620
+ onboardingAny.passkeyDivider.style.display = "none";
2621
+ }
2622
+ if (onboardingAny?.externalWalletDivider) {
2623
+ onboardingAny.externalWalletDivider.style.display = "none";
2624
+ }
2625
+ if (onboardingAny?.divider) {
2626
+ onboardingAny.divider.style.display = "none";
2627
+ }
2628
+ }, onHideMoreWallets: () => {
2629
+ const onboardingAny = onboardingRef.current;
2630
+ const authMethods = onboardingAny?.config?.authMethods || [
2631
+ "otp",
2632
+ "google",
2633
+ ];
2634
+ const showEmail = authMethods.includes("otp");
2635
+ const showGoogle = authMethods.includes("google");
2636
+ const showPasskey = authMethods.includes("passkey");
2637
+ if (onboardingAny?.emailForm) {
2638
+ onboardingAny.emailForm.style.display = showEmail ? "" : "none";
2639
+ }
2640
+ if (onboardingAny?.googleButton) {
2641
+ onboardingAny.googleButton.style.display = showGoogle
2642
+ ? ""
2643
+ : "none";
2644
+ }
2645
+ // Show passkey controls if enabled
2646
+ if (onboardingAny?.passkeyLoginButton) {
2647
+ onboardingAny.passkeyLoginButton.style.display = showPasskey
2648
+ ? ""
2649
+ : "none";
2650
+ }
2651
+ if (onboardingAny?.passkeySignupLink) {
2652
+ onboardingAny.passkeySignupLink.style.display = showPasskey
2653
+ ? ""
2654
+ : "none";
2655
+ }
2656
+ if (onboardingAny?.passkeyErrorElement) {
2657
+ onboardingAny.passkeyErrorElement.style.display = "none";
2658
+ }
2659
+ // Show passkey divider if passkey is enabled and there are other auth methods
2660
+ if (onboardingAny?.passkeyDivider) {
2661
+ onboardingAny.passkeyDivider.style.display =
2662
+ showPasskey && (showEmail || showGoogle) ? "" : "none";
2663
+ }
2664
+ if (onboardingAny?.divider) {
2665
+ onboardingAny.divider.style.display =
2666
+ showEmail && showGoogle ? "" : "none";
2667
+ }
2668
+ const hasEmailOrGoogle = showEmail || showGoogle;
2669
+ if (onboardingAny?.externalWalletDivider && hasEmailOrGoogle) {
2670
+ onboardingAny.externalWalletDivider.style.display = "";
2671
+ }
2672
+ else if (onboardingAny?.externalWalletDivider) {
2673
+ onboardingAny.externalWalletDivider.style.display = "none";
2674
+ }
2675
+ } }) }));
2676
+ if (rootStillValid) {
2677
+ externalWalletRootRef.current.render(renderExternalWalletButtons());
2678
+ externalWalletMountedRef.current = true;
2679
+ }
2680
+ else {
2681
+ try {
2682
+ // If we have an old root but the container changed or is invalid, unmount it first
2683
+ if (externalWalletRootRef.current) {
2684
+ try {
2685
+ // Attempt to unmount if possible, though react-dom/client roots don't have unmount in the same way as legacy
2686
+ // But we should at least clear the ref
2687
+ externalWalletRootRef.current.unmount();
2688
+ }
2689
+ catch (e) {
2690
+ // Ignore unmount errors
2691
+ }
2692
+ externalWalletRootRef.current = null;
2693
+ }
2694
+ const reactDomClient = await import("react-dom/client");
2695
+ const rootInstance = reactDomClient.createRoot(container);
2696
+ externalWalletRootRef.current = rootInstance;
2697
+ externalWalletContainerRef.current = container; // Track the container
2698
+ rootInstance.render(renderExternalWalletButtons());
2699
+ externalWalletMountedRef.current = true;
2700
+ }
2701
+ catch (error) {
2702
+ externalWalletMountedRef.current = false;
2703
+ }
2704
+ }
2705
+ }, []);
2706
+ // Mount ExternalWalletButtons into OnboardingUI container (only when wagmi is available)
2707
+ useEffect(() => {
2708
+ if (!externalWalletsEnabled) {
2709
+ // When disabled, ensure container is hidden
2710
+ const onboardingAny = onboardingRef.current;
2711
+ if (onboardingAny?.externalWalletContainer) {
2712
+ onboardingAny.externalWalletContainer.style.display = "none";
2713
+ }
2714
+ if (onboardingAny?.externalWalletDivider) {
2715
+ onboardingAny.externalWalletDivider.style.display = "none";
2716
+ }
2717
+ return;
2718
+ }
2719
+ if (!onboardingRef.current) {
2720
+ console.log("OnboardingUI not initialized yet");
2721
+ return;
2722
+ }
2723
+ if (!wagmiConnect) {
2724
+ console.log("Wagmi connect not available");
2725
+ return;
2726
+ }
2727
+ if (!wagmiConnect.connectors || wagmiConnect.connectors.length === 0) {
2728
+ console.log("Wagmi connectors not ready yet");
2729
+ return;
2730
+ }
2731
+ if (availableConnectors.length === 0) {
2732
+ // console.log('No available connectors');
2733
+ // Don't return early - still mount the component so it can update when connectors become available
2734
+ // return;
2735
+ }
2736
+ let retryCount = 0;
2737
+ const maxRetries = 50; // Try for up to 2.5 seconds
2738
+ const mountExternalWallets = async () => {
2739
+ const onboardingAny = onboardingRef.current;
2740
+ if (!onboardingAny?.externalWalletContainer) {
2741
+ retryCount++;
2742
+ if (retryCount < maxRetries) {
2743
+ // Retry after a short delay if container not ready
2744
+ setTimeout(mountExternalWallets, 50); // Reduced from 100ms to 50ms
2745
+ }
2746
+ return;
2747
+ }
2748
+ try {
2749
+ const container = onboardingAny.externalWalletContainer;
2750
+ if (!container) {
2751
+ console.warn("External wallet container is null");
2752
+ return;
2753
+ }
2754
+ // Check if we're on OTP screen - if so, don't show external wallets
2755
+ // Check if OTP screen exists AND is actually visible in the DOM
2756
+ const isOtpScreen = onboardingAny.otpVerificationScreen &&
2757
+ onboardingAny.otpVerificationScreen.parentElement &&
2758
+ onboardingAny.otpVerificationScreen.offsetParent !== null;
2759
+ if (!isOtpScreen) {
2760
+ // Only show external wallets if not on OTP screen
2761
+ // Force container to be visible when external wallets are enabled
2762
+ container.style.display = "";
2763
+ // Show divider only if both email/Google AND external wallets are visible
2764
+ const authMethods = onboardingAny.config?.authMethods || [
2765
+ "otp",
2766
+ "google",
2767
+ ];
2768
+ const showEmail = authMethods.includes("otp");
2769
+ const showGoogle = authMethods.includes("google");
2770
+ const hasEmailOrGoogle = showEmail || showGoogle;
2771
+ if (onboardingAny.externalWalletDivider && hasEmailOrGoogle) {
2772
+ onboardingAny.externalWalletDivider.style.display = "";
2773
+ }
2774
+ else if (onboardingAny.externalWalletDivider) {
2775
+ onboardingAny.externalWalletDivider.style.display = "none";
2776
+ }
2777
+ }
2778
+ else {
2779
+ // On OTP screen - hide external wallets
2780
+ container.style.display = "none";
2781
+ if (onboardingAny.externalWalletDivider) {
2782
+ onboardingAny.externalWalletDivider.style.display = "none";
2783
+ }
2784
+ }
2785
+ // Reuse existing root if container matches to avoid unmount/remount race conditions
2786
+ let rootInstance = externalWalletRootRef.current;
2787
+ const reactDomClient = await import("react-dom/client");
2788
+ if (rootInstance && externalWalletContainerRef.current === container) {
2789
+ // Reuse existing root - no need to unmount/remount
2790
+ }
2791
+ else {
2792
+ // Unmount previous root if it exists (and container is different)
2793
+ if (rootInstance) {
2794
+ try {
2795
+ // Wrap unmount in setTimeout to avoid "synchronously unmount... while rendering" error
2796
+ // This happens if unmount is called during a render cycle
2797
+ const oldRoot = rootInstance;
2798
+ setTimeout(() => {
2799
+ try {
2800
+ oldRoot.unmount();
2801
+ }
2802
+ catch (e) {
2803
+ // Ignore
2804
+ }
2805
+ }, 0);
2806
+ }
2807
+ catch (e) {
2808
+ // Ignore unmount errors
2809
+ }
2810
+ externalWalletRootRef.current = null;
2811
+ externalWalletContainerRef.current = null;
2812
+ }
2813
+ // Create new root
2814
+ rootInstance = reactDomClient.createRoot(container);
2815
+ externalWalletRootRef.current = rootInstance;
2816
+ externalWalletContainerRef.current = container; // Track the container
2817
+ }
2818
+ // Render the ExternalWalletButtons component
2819
+ rootInstance.render(_jsx(AbstraxnContext.Provider, { value: valueRef.current, children: _jsx(ExternalWalletButtons, { onConnect: async (_connectorId, _address) => {
2820
+ // console.log('External wallet connected:', connectorId, address);
2821
+ // Modal will be closed automatically by the useEffect that watches wagmiAccount
2822
+ }, onShowMoreWallets: () => {
2823
+ // Hide email and Google sections when "More wallets" is clicked
2824
+ const onboardingAny = onboardingRef.current;
2825
+ if (onboardingAny?.emailForm) {
2826
+ onboardingAny.emailForm.style.display = "none";
2827
+ }
2828
+ if (onboardingAny?.googleButton) {
2829
+ onboardingAny.googleButton.style.display = "none";
2830
+ }
2831
+ // Hide all passkey elements when wallet selection screen is shown
2832
+ if (onboardingAny?.passkeyLoginButton) {
2833
+ onboardingAny.passkeyLoginButton.style.display = "none";
2834
+ }
2835
+ if (onboardingAny?.passkeySignupLink) {
2836
+ onboardingAny.passkeySignupLink.style.display = "none";
2837
+ }
2838
+ if (onboardingAny?.passkeyErrorElement) {
2839
+ onboardingAny.passkeyErrorElement.style.display = "none";
2840
+ }
2841
+ if (onboardingAny?.passkeyDivider) {
2842
+ onboardingAny.passkeyDivider.style.display = "none";
2843
+ }
2844
+ if (onboardingAny?.externalWalletDivider) {
2845
+ onboardingAny.externalWalletDivider.style.display = "none";
2846
+ }
2847
+ // Also hide the divider between email and Google if it exists
2848
+ if (onboardingAny?.divider) {
2849
+ onboardingAny.divider.style.display = "none";
2850
+ }
2851
+ }, onHideMoreWallets: () => {
2852
+ // Show email and Google sections when back is clicked (restore initial screen)
2853
+ const onboardingAny = onboardingRef.current;
2854
+ const authMethods = onboardingAny?.config?.authMethods || [
2855
+ "otp",
2856
+ "google",
2857
+ ];
2858
+ const showEmail = authMethods.includes("otp");
2859
+ const showGoogle = authMethods.includes("google");
2860
+ const showPasskey = authMethods.includes("passkey");
2861
+ // Show email form if enabled
2862
+ if (onboardingAny?.emailForm) {
2863
+ onboardingAny.emailForm.style.display = showEmail
2864
+ ? ""
2865
+ : "none";
2866
+ }
2867
+ // Show Google button if enabled
2868
+ if (onboardingAny?.googleButton) {
2869
+ onboardingAny.googleButton.style.display = showGoogle
2870
+ ? ""
2871
+ : "none";
2872
+ }
2873
+ // Show passkey controls if enabled
2874
+ if (onboardingAny?.passkeyLoginButton) {
2875
+ onboardingAny.passkeyLoginButton.style.display = showPasskey
2876
+ ? ""
2877
+ : "none";
2878
+ }
2879
+ if (onboardingAny?.passkeySignupLink) {
2880
+ onboardingAny.passkeySignupLink.style.display = showPasskey
2881
+ ? ""
2882
+ : "none";
2883
+ }
2884
+ if (onboardingAny?.passkeyErrorElement) {
2885
+ onboardingAny.passkeyErrorElement.style.display = "none";
2886
+ }
2887
+ // Show passkey divider if passkey is enabled and there are other auth methods
2888
+ if (onboardingAny?.passkeyDivider) {
2889
+ onboardingAny.passkeyDivider.style.display =
2890
+ showPasskey && (showEmail || showGoogle) ? "" : "none";
2891
+ }
2892
+ // Show divider between email and Google if both are enabled
2893
+ if (onboardingAny?.divider) {
2894
+ onboardingAny.divider.style.display =
2895
+ showEmail && showGoogle ? "" : "none";
2896
+ }
2897
+ // Show the "or" divider before external wallets only if email/Google are visible
2898
+ const hasEmailOrGoogle = showEmail || showGoogle;
2899
+ if (onboardingAny?.externalWalletDivider && hasEmailOrGoogle) {
2900
+ onboardingAny.externalWalletDivider.style.display = "";
2901
+ }
2902
+ else if (onboardingAny?.externalWalletDivider) {
2903
+ onboardingAny.externalWalletDivider.style.display = "none";
2904
+ }
2905
+ } }) }));
2906
+ externalWalletMountedRef.current = true;
2907
+ }
2908
+ catch (error) {
2909
+ console.error("Failed to mount ExternalWalletButtons:", error);
2910
+ externalWalletMountedRef.current = false;
2911
+ }
2912
+ };
2913
+ // When externalWalletsEnabled changes, force a fresh mount
2914
+ // This ensures buttons appear immediately without requiring a refresh
2915
+ const forceRemount = () => {
2916
+ // Unmount existing root if it exists
2917
+ if (externalWalletRootRef.current) {
2918
+ try {
2919
+ externalWalletRootRef.current.unmount();
2920
+ }
2921
+ catch (e) {
2922
+ // Ignore unmount errors
2923
+ }
2924
+ externalWalletRootRef.current = null;
2925
+ }
2926
+ externalWalletContainerRef.current = null;
2927
+ externalWalletMountedRef.current = false;
2928
+ };
2929
+ // Start mounting immediately - no delay needed
2930
+ mountExternalWallets();
2931
+ return () => {
2932
+ // Cleanup if needed
2933
+ };
2934
+ }, [
2935
+ externalWalletsEnabled,
2936
+ wagmiConnect,
2937
+ wagmiConnect?.connectors,
2938
+ availableConnectors.length,
2939
+ ]);
2940
+ // Update the rendered component when value changes (but don't re-mount)
2941
+ useEffect(() => {
2942
+ if (!externalWalletsEnabled)
2943
+ return;
2944
+ if (!externalWalletRootRef.current || !externalWalletMountedRef.current)
2945
+ return;
2946
+ // Update the rendered component with latest value
2947
+ try {
2948
+ externalWalletRootRef.current.render(_jsx(AbstraxnContext.Provider, { value: valueRef.current, children: _jsx(ExternalWalletButtons, { onConnect: async (_connectorId, _address) => {
2949
+ // console.log('External wallet connected:', connectorId, address);
2950
+ }, onShowMoreWallets: () => {
2951
+ // Hide email and Google sections when "More wallets" is clicked
2952
+ const onboardingAny = onboardingRef.current;
2953
+ if (onboardingAny?.emailForm) {
2954
+ onboardingAny.emailForm.style.display = "none";
2955
+ }
2956
+ if (onboardingAny?.googleButton) {
2957
+ onboardingAny.googleButton.style.display = "none";
2958
+ }
2959
+ // Hide Discord and X (Twitter) buttons when wallet selection screen is shown
2960
+ if (onboardingAny?.discordButton) {
2961
+ onboardingAny.discordButton.style.display = "none";
2962
+ }
2963
+ if (onboardingAny?.twitterButton) {
2964
+ onboardingAny.twitterButton.style.display = "none";
2965
+ }
2966
+ // Hide social grid if it exists
2967
+ if (onboardingAny?.socialGrid) {
2968
+ onboardingAny.socialGrid.style.display = "none";
2969
+ }
2970
+ // Extra guard: hide any social icon buttons by class
2971
+ if (onboardingAny?.rootElement) {
2972
+ onboardingAny.rootElement
2973
+ .querySelectorAll(".onboarding-button-social-icon, .onboarding-button-social")
2974
+ .forEach((el) => {
2975
+ el.style.display = "none";
2976
+ });
2977
+ }
2978
+ // Hide all passkey elements when wallet selection screen is shown
2979
+ if (onboardingAny?.passkeyLoginButton) {
2980
+ onboardingAny.passkeyLoginButton.style.display = "none";
2981
+ }
2982
+ if (onboardingAny?.passkeySignupLink) {
2983
+ onboardingAny.passkeySignupLink.style.display = "none";
2984
+ }
2985
+ if (onboardingAny?.passkeyErrorElement) {
2986
+ onboardingAny.passkeyErrorElement.style.display = "none";
2987
+ }
2988
+ if (onboardingAny?.passkeyDivider) {
2989
+ onboardingAny.passkeyDivider.style.display = "none";
2990
+ }
2991
+ if (onboardingAny?.externalWalletDivider) {
2992
+ onboardingAny.externalWalletDivider.style.display = "none";
2993
+ }
2994
+ // Also hide the divider between email and Google if it exists
2995
+ if (onboardingAny?.divider) {
2996
+ onboardingAny.divider.style.display = "none";
2997
+ }
2998
+ }, onHideMoreWallets: () => {
2999
+ // Show email and Google sections when back is clicked
3000
+ const onboardingAny = onboardingRef.current;
3001
+ const authMethods = onboardingAny?.config?.authMethods || [
3002
+ "otp",
3003
+ "google",
3004
+ ];
3005
+ const showEmail = authMethods.includes("otp");
3006
+ const showGoogle = authMethods.includes("google");
3007
+ const showTwitter = authMethods.includes("twitter");
3008
+ const showDiscord = authMethods.includes("discord");
3009
+ const showPasskey = authMethods.includes("passkey");
3010
+ if (onboardingAny?.emailForm) {
3011
+ onboardingAny.emailForm.style.display = showEmail ? "" : "none";
3012
+ }
3013
+ if (onboardingAny?.googleButton) {
3014
+ onboardingAny.googleButton.style.display = showGoogle
3015
+ ? ""
3016
+ : "none";
3017
+ }
3018
+ // Show Discord and X (Twitter) buttons if enabled
3019
+ if (onboardingAny?.discordButton) {
3020
+ onboardingAny.discordButton.style.display = showDiscord
3021
+ ? ""
3022
+ : "none";
3023
+ }
3024
+ if (onboardingAny?.twitterButton) {
3025
+ onboardingAny.twitterButton.style.display = showTwitter
3026
+ ? ""
3027
+ : "none";
3028
+ }
3029
+ // Show social grid if it exists and social methods are enabled
3030
+ if (onboardingAny?.socialGrid) {
3031
+ const hasSocialMethods = showGoogle || showTwitter || showDiscord;
3032
+ onboardingAny.socialGrid.style.display = hasSocialMethods
3033
+ ? ""
3034
+ : "none";
3035
+ }
3036
+ // Extra guard: restore social icon buttons by class based on flags
3037
+ if (onboardingAny?.rootElement) {
3038
+ const hasSocialMethods = showGoogle || showTwitter || showDiscord;
3039
+ onboardingAny.rootElement
3040
+ .querySelectorAll(".onboarding-button-social-icon, .onboarding-button-social")
3041
+ .forEach((el) => {
3042
+ el.style.display = hasSocialMethods
3043
+ ? ""
3044
+ : "none";
3045
+ });
3046
+ }
3047
+ // Show passkey controls if enabled
3048
+ if (onboardingAny?.passkeyLoginButton) {
3049
+ onboardingAny.passkeyLoginButton.style.display = showPasskey
3050
+ ? ""
3051
+ : "none";
3052
+ }
3053
+ if (onboardingAny?.passkeySignupLink) {
3054
+ onboardingAny.passkeySignupLink.style.display = showPasskey
3055
+ ? ""
3056
+ : "none";
3057
+ }
3058
+ if (onboardingAny?.passkeyErrorElement) {
3059
+ onboardingAny.passkeyErrorElement.style.display = "none";
3060
+ }
3061
+ // Show passkey divider if passkey is enabled and there are other auth methods
3062
+ if (onboardingAny?.passkeyDivider) {
3063
+ onboardingAny.passkeyDivider.style.display =
3064
+ showPasskey &&
3065
+ (showEmail || showGoogle || showTwitter || showDiscord)
3066
+ ? ""
3067
+ : "none";
3068
+ }
3069
+ // Show divider between email and social methods if both are enabled
3070
+ if (onboardingAny?.divider) {
3071
+ const hasSocialMethods = showGoogle || showTwitter || showDiscord;
3072
+ onboardingAny.divider.style.display =
3073
+ showEmail && hasSocialMethods ? "" : "none";
3074
+ }
3075
+ if (onboardingAny?.externalWalletDivider) {
3076
+ onboardingAny.externalWalletDivider.style.display = "";
3077
+ }
3078
+ } }) }));
3079
+ }
3080
+ catch (error) {
3081
+ console.warn("Failed to update ExternalWalletButtons:", error);
3082
+ }
3083
+ }, [value, externalWalletsEnabled]);
3084
+ return (_jsx(AbstraxnContext.Provider, { value: value, children: children }));
3085
+ }
3086
+ //# sourceMappingURL=AbstraxnProviderInner.js.map