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