@openfort/react-native 0.1.19 → 0.1.21

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 (47) hide show
  1. package/dist/components/AuthBoundary.js +4 -1
  2. package/dist/core/index.js +1 -1
  3. package/dist/core/provider.js +3 -27
  4. package/dist/hooks/auth/useEmailAuth.js +90 -3
  5. package/dist/hooks/core/useOpenfort.js +3 -17
  6. package/dist/hooks/wallet/index.js +4 -2
  7. package/dist/hooks/wallet/solanaProvider.js +77 -0
  8. package/dist/hooks/wallet/useEmbeddedEthereumWallet.js +465 -0
  9. package/dist/hooks/wallet/useEmbeddedSolanaWallet.js +391 -0
  10. package/dist/hooks/wallet/utils.js +75 -0
  11. package/dist/index.js +1 -1
  12. package/dist/lib/hookConsistency.js +6 -4
  13. package/dist/native/oauth.js +13 -0
  14. package/dist/native/storage.js +4 -0
  15. package/dist/native/webview.js +15 -1
  16. package/dist/types/components/AuthBoundary.d.ts +1 -0
  17. package/dist/types/core/index.d.ts +1 -1
  18. package/dist/types/core/provider.d.ts +1 -10
  19. package/dist/types/hooks/auth/useEmailAuth.d.ts +6 -7
  20. package/dist/types/hooks/auth/useGuestAuth.d.ts +1 -2
  21. package/dist/types/hooks/auth/useOAuth.d.ts +1 -2
  22. package/dist/types/hooks/core/useOpenfort.d.ts +2 -13
  23. package/dist/types/hooks/wallet/index.d.ts +2 -1
  24. package/dist/types/hooks/wallet/solanaProvider.d.ts +75 -0
  25. package/dist/types/hooks/wallet/useEmbeddedEthereumWallet.d.ts +53 -0
  26. package/dist/types/hooks/wallet/useEmbeddedSolanaWallet.d.ts +47 -0
  27. package/dist/types/hooks/wallet/utils.d.ts +17 -0
  28. package/dist/types/index.d.ts +1 -1
  29. package/dist/types/index.js +1 -2
  30. package/dist/types/lib/hookConsistency.d.ts +6 -0
  31. package/dist/types/native/oauth.d.ts +13 -0
  32. package/dist/types/native/storage.d.ts +4 -0
  33. package/dist/types/native/webview.d.ts +14 -0
  34. package/dist/types/types/auth.d.ts +0 -56
  35. package/dist/types/types/hookOption.d.ts +0 -1
  36. package/dist/types/types/index.d.ts +3 -30
  37. package/dist/types/types/oauth.d.ts +0 -38
  38. package/dist/types/types/wallet.d.ts +120 -216
  39. package/package.json +1 -1
  40. package/dist/hooks/auth/useCreateWalletPostAuth.js +0 -34
  41. package/dist/hooks/wallet/useWallets.js +0 -437
  42. package/dist/types/config.js +0 -1
  43. package/dist/types/hooks/auth/useCreateWalletPostAuth.d.ts +0 -1
  44. package/dist/types/hooks/wallet/useWallets.d.ts +0 -78
  45. package/dist/types/predicates.js +0 -120
  46. package/dist/types/types/config.d.ts +0 -59
  47. package/dist/types/types/predicates.d.ts +0 -118
@@ -0,0 +1,465 @@
1
+ import { AccountTypeEnum, ChainTypeEnum, EmbeddedState } from '@openfort/openfort-js';
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ import { useOpenfortContext } from '../../core/context';
4
+ import { onError, onSuccess } from '../../lib/hookConsistency';
5
+ import { logger } from '../../lib/logger';
6
+ import { OpenfortError, OpenfortErrorType } from '../../types/openfortError';
7
+ import { buildRecoveryParams } from './utils';
8
+ /**
9
+ * Hook for managing embedded Ethereum wallets.
10
+ *
11
+ * This hook provides comprehensive Ethereum wallet management including creation, activation,
12
+ * and recovery. It returns a discriminated union state that enables type-safe wallet interactions.
13
+ *
14
+ * @param options - Configuration with default chainId and callback functions
15
+ * @returns Discriminated union state object. The `status` field determines available properties.
16
+ * Possible states: 'disconnected', 'connecting', 'reconnecting', 'creating', 'needs-recovery',
17
+ * 'connected', 'error'. When connected, includes `provider` and `activeWallet`. All states include
18
+ * `create`, `setActive`, `setRecovery`, `exportPrivateKey`, and `wallets` methods/properties.
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * import { useEmbeddedEthereumWallet, isConnected, isLoading } from '@openfort/react-native';
23
+ *
24
+ * const ethereum = useEmbeddedEthereumWallet({
25
+ * chainId: 1,
26
+ * onCreateSuccess: (account, provider) => console.log('Wallet created:', account.address),
27
+ * });
28
+ *
29
+ * if (isLoading(ethereum)) {
30
+ * return <ActivityIndicator />;
31
+ * }
32
+ *
33
+ * if (isConnected(ethereum)) {
34
+ * // TypeScript knows provider and activeWallet are available
35
+ * const tx = await ethereum.provider.request({
36
+ * method: 'eth_sendTransaction',
37
+ * params: [{ from: ethereum.activeWallet.address, to: '0x...', value: '0x0' }]
38
+ * });
39
+ * }
40
+ *
41
+ * // Create wallet if none exist
42
+ * if (ethereum.status === 'disconnected' && ethereum.wallets.length === 0) {
43
+ * await ethereum.create({ chainId: 1 });
44
+ * }
45
+ * ```
46
+ */
47
+ export function useEmbeddedEthereumWallet(options = {}) {
48
+ const { client, supportedChains, walletConfig, embeddedState } = useOpenfortContext();
49
+ const [embeddedAccounts, setEmbeddedAccounts] = useState([]);
50
+ const [activeWalletId, setActiveWalletId] = useState(null);
51
+ const [activeAccount, setActiveAccount] = useState(null);
52
+ const [provider, setProvider] = useState(null);
53
+ const recoverPromiseRef = useRef(null);
54
+ const [status, setStatus] = useState({
55
+ status: 'idle',
56
+ });
57
+ // Fetch Ethereum embedded accounts
58
+ const fetchEmbeddedAccounts = useCallback(async () => {
59
+ if (!client || embeddedState === EmbeddedState.NONE || embeddedState === EmbeddedState.UNAUTHENTICATED) {
60
+ setEmbeddedAccounts([]);
61
+ return;
62
+ }
63
+ try {
64
+ const accounts = await client.embeddedWallet.list({
65
+ limit: 100,
66
+ chainType: ChainTypeEnum.EVM,
67
+ accountType: walletConfig?.accountType === AccountTypeEnum.EOA ? undefined : AccountTypeEnum.SMART_ACCOUNT,
68
+ });
69
+ // Filter for Ethereum accounts only
70
+ setEmbeddedAccounts(accounts);
71
+ }
72
+ catch {
73
+ setEmbeddedAccounts([]);
74
+ }
75
+ }, [client, embeddedState, walletConfig]);
76
+ useEffect(() => {
77
+ fetchEmbeddedAccounts();
78
+ }, [fetchEmbeddedAccounts]);
79
+ // Sync active wallet ID and account with client
80
+ useEffect(() => {
81
+ ;
82
+ (async () => {
83
+ try {
84
+ const embeddedAccount = await client.embeddedWallet.get();
85
+ if (embeddedAccount.chainType === ChainTypeEnum.EVM) {
86
+ setActiveWalletId(embeddedAccount.id);
87
+ setActiveAccount(embeddedAccount);
88
+ }
89
+ else {
90
+ setActiveWalletId(null);
91
+ setActiveAccount(null);
92
+ }
93
+ }
94
+ catch {
95
+ setActiveWalletId(null);
96
+ setActiveAccount(null);
97
+ }
98
+ })();
99
+ }, [client]);
100
+ // Get Ethereum provider
101
+ const getEthereumProvider = useCallback(async () => {
102
+ const resolvePolicy = () => {
103
+ const ethereumProviderPolicyId = walletConfig?.ethereumProviderPolicyId;
104
+ if (!ethereumProviderPolicyId)
105
+ return undefined;
106
+ if (typeof ethereumProviderPolicyId === 'string') {
107
+ return ethereumProviderPolicyId;
108
+ }
109
+ if (!options.chainId)
110
+ return undefined;
111
+ const policy = ethereumProviderPolicyId[options.chainId];
112
+ if (!policy) {
113
+ return undefined;
114
+ }
115
+ return policy;
116
+ };
117
+ return await client.embeddedWallet.getEthereumProvider({ announceProvider: false, policy: resolvePolicy() });
118
+ }, [client.embeddedWallet, walletConfig, options.chainId]);
119
+ // Initialize provider when recovering an active wallet on mount
120
+ useEffect(() => {
121
+ // Only initialize if we have an account but no provider
122
+ if (!activeAccount || provider) {
123
+ return;
124
+ }
125
+ // Don't interfere with user-initiated actions
126
+ if (['creating', 'connecting', 'reconnecting', 'loading'].includes(status.status)) {
127
+ return;
128
+ }
129
+ // Only initialize if embedded state is ready
130
+ if (embeddedState !== EmbeddedState.READY) {
131
+ return;
132
+ }
133
+ ;
134
+ (async () => {
135
+ try {
136
+ logger.info('Initializing provider for recovered Ethereum wallet session');
137
+ setStatus({ status: 'connecting' });
138
+ const ethProvider = await getEthereumProvider();
139
+ setProvider(ethProvider);
140
+ setStatus({ status: 'success' });
141
+ }
142
+ catch (e) {
143
+ const error = e instanceof OpenfortError
144
+ ? e
145
+ : new OpenfortError('Failed to initialize provider for active Ethereum wallet', OpenfortErrorType.WALLET_ERROR, { error: e });
146
+ logger.error('Ethereum provider initialization failed', error);
147
+ setStatus({ status: 'error', error });
148
+ }
149
+ })();
150
+ }, [activeAccount, provider, embeddedState, status.status, getEthereumProvider]);
151
+ // Build wallets list with deduplication logic
152
+ const wallets = useMemo(() => {
153
+ // Deduplicate accounts based on account type
154
+ const deduplicatedAccounts = embeddedAccounts.reduce((acc, account) => {
155
+ if (walletConfig?.accountType === AccountTypeEnum.EOA) {
156
+ // For EOAs, deduplicate by address only (EOAs work across all chains)
157
+ if (!acc.some((a) => a.address.toLowerCase() === account.address.toLowerCase())) {
158
+ acc.push(account);
159
+ }
160
+ }
161
+ else {
162
+ // For Smart Accounts, keep separate entries per chain (they're chain-specific)
163
+ // Only deduplicate exact matches (same address AND same chainId)
164
+ if (!acc.some((a) => a.address.toLowerCase() === account.address.toLowerCase() && a.chainId === account.chainId)) {
165
+ acc.push(account);
166
+ }
167
+ }
168
+ return acc;
169
+ }, []);
170
+ return deduplicatedAccounts.map((account, index) => ({
171
+ address: account.address,
172
+ ownerAddress: account.ownerAddress,
173
+ implementationType: account.implementationType,
174
+ chainType: ChainTypeEnum.EVM,
175
+ walletIndex: index,
176
+ getProvider: async () => await getEthereumProvider(),
177
+ }));
178
+ }, [embeddedAccounts, walletConfig?.accountType, getEthereumProvider]);
179
+ // Create wallet action
180
+ const create = useCallback(async (createOptions) => {
181
+ logger.info('Creating Ethereum wallet with options', createOptions);
182
+ try {
183
+ setStatus({ status: 'creating' });
184
+ // Validate chainId
185
+ let chainId;
186
+ if (createOptions?.chainId) {
187
+ if (!supportedChains || !supportedChains.some((chain) => chain.id === createOptions.chainId)) {
188
+ throw new OpenfortError(`Chain ID ${createOptions.chainId} is not supported. Supported chains: ${supportedChains?.map((c) => c.id).join(', ') || 'none'}`, OpenfortErrorType.WALLET_ERROR);
189
+ }
190
+ chainId = createOptions.chainId;
191
+ }
192
+ else if (options.chainId) {
193
+ chainId = options.chainId;
194
+ }
195
+ else if (supportedChains && supportedChains.length > 0) {
196
+ chainId = supportedChains[0].id;
197
+ }
198
+ else {
199
+ throw new OpenfortError('No supported chains available for wallet creation', OpenfortErrorType.WALLET_ERROR);
200
+ }
201
+ // Build recovery params
202
+ const recoveryParams = await buildRecoveryParams(createOptions, walletConfig);
203
+ // Create embedded wallet
204
+ const embeddedAccount = await client.embeddedWallet.create({
205
+ chainId,
206
+ accountType: createOptions?.accountType || walletConfig?.accountType || AccountTypeEnum.SMART_ACCOUNT,
207
+ chainType: ChainTypeEnum.EVM,
208
+ recoveryParams,
209
+ });
210
+ logger.info('Embedded Ethereum wallet created');
211
+ // Get provider
212
+ const ethProvider = await getEthereumProvider();
213
+ setProvider(ethProvider);
214
+ // Refresh accounts and set as active
215
+ await fetchEmbeddedAccounts();
216
+ setActiveWalletId(embeddedAccount.id);
217
+ setActiveAccount(embeddedAccount);
218
+ setStatus({ status: 'success' });
219
+ onSuccess({
220
+ options: createOptions,
221
+ data: {
222
+ account: embeddedAccount,
223
+ provider: ethProvider,
224
+ },
225
+ });
226
+ if (createOptions?.onSuccess) {
227
+ createOptions.onSuccess({ account: embeddedAccount, provider: ethProvider });
228
+ }
229
+ if (options.onCreateSuccess) {
230
+ options.onCreateSuccess(embeddedAccount, ethProvider);
231
+ }
232
+ return embeddedAccount;
233
+ }
234
+ catch (e) {
235
+ const error = e instanceof OpenfortError
236
+ ? e
237
+ : new OpenfortError('Failed to create Ethereum wallet', OpenfortErrorType.WALLET_ERROR, { error: e });
238
+ setStatus({ status: 'error', error });
239
+ onError({
240
+ options: createOptions,
241
+ error,
242
+ });
243
+ if (createOptions?.onError) {
244
+ createOptions.onError(error);
245
+ }
246
+ if (options.onCreateError) {
247
+ options.onCreateError(error);
248
+ }
249
+ throw error;
250
+ }
251
+ }, [client, supportedChains, walletConfig, options, getEthereumProvider, fetchEmbeddedAccounts]);
252
+ // Set active wallet action
253
+ const setActive = useCallback(async (setActiveOptions) => {
254
+ // Prevent concurrent recoveries
255
+ if (recoverPromiseRef.current) {
256
+ await recoverPromiseRef.current;
257
+ return;
258
+ }
259
+ if (wallets.length === 0) {
260
+ const error = new OpenfortError('No embedded Ethereum wallets available to set as active', OpenfortErrorType.WALLET_ERROR);
261
+ onError({
262
+ options: setActiveOptions,
263
+ error,
264
+ });
265
+ if (setActiveOptions.onError) {
266
+ setActiveOptions.onError(error);
267
+ }
268
+ if (options.onSetActiveError) {
269
+ options.onSetActiveError(error);
270
+ }
271
+ throw error;
272
+ }
273
+ setStatus({ status: 'connecting' });
274
+ recoverPromiseRef.current = (async () => {
275
+ try {
276
+ // Validate chainId
277
+ let chainId;
278
+ if (setActiveOptions.chainId) {
279
+ if (!supportedChains || !supportedChains.some((chain) => chain.id === setActiveOptions.chainId)) {
280
+ throw new OpenfortError(`Chain ID ${setActiveOptions.chainId} is not supported. Supported chains: ${supportedChains?.map((c) => c.id).join(', ') || 'none'}`, OpenfortErrorType.WALLET_ERROR);
281
+ }
282
+ chainId = setActiveOptions.chainId;
283
+ }
284
+ else if (options.chainId) {
285
+ chainId = options.chainId;
286
+ }
287
+ else if (supportedChains && supportedChains.length > 0) {
288
+ chainId = supportedChains[0].id;
289
+ }
290
+ // Find account to recover
291
+ let embeddedAccountToRecover;
292
+ if (walletConfig?.accountType === AccountTypeEnum.EOA) {
293
+ // For EOAs, match only by address (EOAs work across all chains)
294
+ embeddedAccountToRecover = embeddedAccounts.find((account) => account.address.toLowerCase() === setActiveOptions.address.toLowerCase());
295
+ }
296
+ else {
297
+ // For Smart Accounts, match by both address and chainId (Smart Accounts are chain-specific)
298
+ embeddedAccountToRecover = embeddedAccounts.find((account) => account.chainId === chainId && account.address.toLowerCase() === setActiveOptions.address.toLowerCase());
299
+ }
300
+ if (!embeddedAccountToRecover) {
301
+ const errorMsg = walletConfig?.accountType === AccountTypeEnum.EOA
302
+ ? `No embedded EOA account found for address ${setActiveOptions.address}`
303
+ : `No embedded smart account found for address ${setActiveOptions.address} on chain ID ${chainId}`;
304
+ throw new OpenfortError(errorMsg, OpenfortErrorType.WALLET_ERROR);
305
+ }
306
+ // Build recovery params
307
+ const recoveryParams = await buildRecoveryParams(setActiveOptions, walletConfig);
308
+ // Recover the embedded wallet
309
+ const embeddedAccount = await client.embeddedWallet.recover({
310
+ account: embeddedAccountToRecover.id,
311
+ recoveryParams,
312
+ });
313
+ // Get provider
314
+ const ethProvider = await getEthereumProvider();
315
+ setProvider(ethProvider);
316
+ // Find the wallet index in the deduplicated accounts list
317
+ const walletIndex = embeddedAccounts.findIndex((acc) => acc.address.toLowerCase() === embeddedAccount.address.toLowerCase() &&
318
+ acc.chainId === embeddedAccount.chainId);
319
+ const wallet = {
320
+ address: embeddedAccount.address,
321
+ ownerAddress: embeddedAccount.ownerAddress,
322
+ implementationType: embeddedAccount.implementationType,
323
+ chainType: ChainTypeEnum.EVM,
324
+ walletIndex: walletIndex >= 0 ? walletIndex : 0,
325
+ getProvider: async () => ethProvider,
326
+ };
327
+ recoverPromiseRef.current = null;
328
+ setStatus({ status: 'success' });
329
+ setActiveWalletId(embeddedAccount.id);
330
+ setActiveAccount(embeddedAccount);
331
+ onSuccess({
332
+ options: setActiveOptions,
333
+ data: {
334
+ wallet,
335
+ provider: ethProvider,
336
+ },
337
+ });
338
+ if (setActiveOptions.onSuccess) {
339
+ setActiveOptions.onSuccess({ wallet, provider: ethProvider });
340
+ }
341
+ if (options.onSetActiveSuccess) {
342
+ options.onSetActiveSuccess(wallet, ethProvider);
343
+ }
344
+ return { wallet, provider: ethProvider };
345
+ }
346
+ catch (e) {
347
+ recoverPromiseRef.current = null;
348
+ const error = e instanceof OpenfortError
349
+ ? e
350
+ : new OpenfortError('Failed to set active Ethereum wallet', OpenfortErrorType.WALLET_ERROR);
351
+ setStatus({ status: 'error', error });
352
+ onError({
353
+ options: setActiveOptions,
354
+ error,
355
+ });
356
+ if (setActiveOptions.onError) {
357
+ setActiveOptions.onError(error);
358
+ }
359
+ if (options.onSetActiveError) {
360
+ options.onSetActiveError(error);
361
+ }
362
+ throw error;
363
+ }
364
+ })();
365
+ await recoverPromiseRef.current;
366
+ }, [client, supportedChains, walletConfig, embeddedAccounts, options, wallets.length, getEthereumProvider]);
367
+ // Set recovery method action
368
+ const setRecovery = useCallback(async (params) => {
369
+ try {
370
+ setStatus({ status: 'loading' });
371
+ await client.embeddedWallet.setRecoveryMethod(params.previousRecovery, params.newRecovery);
372
+ setStatus({ status: 'success' });
373
+ onSuccess({
374
+ options: params,
375
+ data: {},
376
+ });
377
+ if (params.onSuccess) {
378
+ params.onSuccess({});
379
+ }
380
+ if (options.onSetRecoverySuccess) {
381
+ options.onSetRecoverySuccess();
382
+ }
383
+ }
384
+ catch (e) {
385
+ const error = new OpenfortError('Failed to set wallet recovery', OpenfortErrorType.WALLET_ERROR, {
386
+ error: e instanceof Error ? e : new Error('Unknown error'),
387
+ });
388
+ setStatus({ status: 'error', error });
389
+ onError({
390
+ options: params,
391
+ error,
392
+ });
393
+ if (params.onError) {
394
+ params.onError(error);
395
+ }
396
+ if (options.onSetRecoveryError) {
397
+ options.onSetRecoveryError(error);
398
+ }
399
+ throw error;
400
+ }
401
+ }, [client, options]);
402
+ // Build active wallet from embeddedWallet.get()
403
+ const activeWallet = useMemo(() => {
404
+ if (!activeWalletId || !activeAccount)
405
+ return null;
406
+ // Find the wallet index in the accounts list
407
+ const accountIndex = embeddedAccounts.findIndex((acc) => acc.id === activeWalletId);
408
+ return {
409
+ address: activeAccount.address,
410
+ ownerAddress: activeAccount.ownerAddress,
411
+ implementationType: activeAccount.implementationType,
412
+ chainType: ChainTypeEnum.EVM,
413
+ walletIndex: accountIndex >= 0 ? accountIndex : 0,
414
+ getProvider: async () => await getEthereumProvider(),
415
+ };
416
+ }, [activeWalletId, activeAccount, embeddedAccounts, getEthereumProvider]);
417
+ // Build discriminated union state
418
+ const state = useMemo(() => {
419
+ const baseActions = {
420
+ create,
421
+ wallets,
422
+ setActive,
423
+ setRecovery,
424
+ exportPrivateKey: client.embeddedWallet.exportPrivateKey,
425
+ };
426
+ // Priority 1: Explicit action states (user-initiated operations)
427
+ if (status.status === 'creating') {
428
+ return { ...baseActions, status: 'creating', activeWallet: null };
429
+ }
430
+ if (status.status === 'connecting' || status.status === 'reconnecting' || status.status === 'loading') {
431
+ return { ...baseActions, status: 'connecting', activeWallet: activeWallet };
432
+ }
433
+ if (status.status === 'error') {
434
+ return { ...baseActions, status: 'error', activeWallet, error: status.error?.message || 'Unknown error' };
435
+ }
436
+ // Priority 2: Check authentication state from context
437
+ if (embeddedState !== EmbeddedState.READY && embeddedState !== EmbeddedState.CREATING_ACCOUNT) {
438
+ // Not authenticated or no embedded wallet capability
439
+ return { ...baseActions, status: 'disconnected', activeWallet: null };
440
+ }
441
+ // Priority 3: Data-driven connection state
442
+ if (activeWallet && provider) {
443
+ // Fully connected - have both wallet and provider
444
+ return { ...baseActions, status: 'connected', activeWallet, provider };
445
+ }
446
+ if (activeAccount && !provider) {
447
+ // Have wallet but provider not initialized yet (mount recovery in progress)
448
+ return { ...baseActions, status: 'connecting', activeWallet: activeWallet };
449
+ }
450
+ // Default: disconnected (authenticated but no wallet selected)
451
+ return { ...baseActions, status: 'disconnected', activeWallet: null };
452
+ }, [
453
+ status,
454
+ activeWallet,
455
+ activeAccount,
456
+ provider,
457
+ wallets,
458
+ embeddedState,
459
+ create,
460
+ setActive,
461
+ setRecovery,
462
+ client.embeddedWallet,
463
+ ]);
464
+ return state;
465
+ }