@openfort/react-native 1.0.5 → 1.0.6

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.
@@ -2,7 +2,7 @@ import { EmbeddedState, ShieldConfiguration, } from '@openfort/openfort-js';
2
2
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
3
3
  import { validateEnvironment } from '../lib/environmentValidation';
4
4
  import { getEmbeddedStateName, logger } from '../lib/logger';
5
- import { EmbeddedWalletWebView, WebViewUtils } from '../native';
5
+ import { EmbeddedWalletWebView, NativePasskeyHandler, WebViewUtils } from '../native';
6
6
  import { createOpenfortClient, setDefaultClient } from './client';
7
7
  import { OpenfortContext } from './context';
8
8
  /**
@@ -100,7 +100,20 @@ export const OpenfortProvider = ({ children, publishableKey, supportedChains, wa
100
100
  logger.printVerboseWarning();
101
101
  logger.setVerbose(verbose);
102
102
  }, [verbose]);
103
- // Create or use provided client
103
+ // Create passkey handler if passkey recovery is configured (single instance for SDK and WebView)
104
+ const passkeyHandler = useMemo(() => {
105
+ if (walletConfig?.passkeyRpId) {
106
+ if (!walletConfig.passkeyRpName) {
107
+ logger.warn('passkeyRpName is required when passkeyRpId is provided for passkey recovery');
108
+ }
109
+ return new NativePasskeyHandler({
110
+ rpId: walletConfig.passkeyRpId,
111
+ rpName: walletConfig.passkeyRpName ?? walletConfig.passkeyRpId,
112
+ });
113
+ }
114
+ return undefined;
115
+ }, [walletConfig?.passkeyRpId, walletConfig?.passkeyRpName]);
116
+ // Create client with passkeyHandler in overrides when configured
104
117
  const client = useMemo(() => {
105
118
  const newClient = createOpenfortClient({
106
119
  baseConfiguration: {
@@ -110,14 +123,19 @@ export const OpenfortProvider = ({ children, publishableKey, supportedChains, wa
110
123
  ? new ShieldConfiguration({
111
124
  shieldPublishableKey: walletConfig.shieldPublishableKey,
112
125
  shieldDebug: walletConfig.debug,
126
+ passkeyRpId: walletConfig.passkeyRpId,
127
+ passkeyRpName: walletConfig.passkeyRpName,
113
128
  })
114
129
  : undefined,
115
- overrides,
130
+ overrides: {
131
+ ...overrides,
132
+ ...(passkeyHandler && { passkeyHandler }),
133
+ },
116
134
  thirdPartyAuth,
117
135
  });
118
136
  setDefaultClient(newClient);
119
137
  return newClient;
120
- }, [publishableKey, walletConfig, overrides]);
138
+ }, [publishableKey, walletConfig, overrides, thirdPartyAuth, passkeyHandler]);
121
139
  // Embedded state
122
140
  const [embeddedState, setEmbeddedState] = useState(EmbeddedState.NONE);
123
141
  // Start polling embedded state: only update and log when state changes
@@ -283,7 +301,7 @@ export const OpenfortProvider = ({ children, publishableKey, supportedChains, wa
283
301
  ]);
284
302
  return (React.createElement(OpenfortContext.Provider, { value: contextValue },
285
303
  children,
286
- client && isReady && WebViewUtils.isSupported() && (React.createElement(EmbeddedWalletWebView, { client: client, isClientReady: isReady, onProxyStatusChange: (status) => {
304
+ client && isReady && WebViewUtils.isSupported() && (React.createElement(EmbeddedWalletWebView, { client: client, isClientReady: isReady, debug: walletConfig?.debug, onProxyStatusChange: (status) => {
287
305
  // Handle WebView status changes for debugging
288
306
  if (verbose) {
289
307
  logger.debug('WebView status changed', status);
@@ -6,4 +6,5 @@
6
6
  // Core SDK hooks
7
7
  export { useOpenfort } from './useOpenfort';
8
8
  export { useOpenfortClient } from './useOpenfortClient';
9
+ export { usePasskeySupport } from './usePasskeySupport';
9
10
  export { useUser } from './useUser';
@@ -0,0 +1,34 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { isPasskeySupported } from '../../native/passkey';
3
+ /**
4
+ * Hook to detect if the platform supports passkeys (WebAuthn).
5
+ *
6
+ * Note: This only checks basic passkey support, not PRF extension support.
7
+ * PRF support can only be determined during passkey creation via the
8
+ * `clientExtensionResults.prf.enabled` field in the response.
9
+ *
10
+ * @returns Object with `isSupported` boolean and `isLoading` state
11
+ */
12
+ export function usePasskeySupport() {
13
+ const [isSupported, setIsSupported] = useState(false);
14
+ const [isLoading, setIsLoading] = useState(true);
15
+ useEffect(() => {
16
+ async function checkSupport() {
17
+ try {
18
+ const available = await isPasskeySupported();
19
+ setIsSupported(available);
20
+ }
21
+ catch {
22
+ setIsSupported(false);
23
+ }
24
+ finally {
25
+ setIsLoading(false);
26
+ }
27
+ }
28
+ checkSupport();
29
+ }, []);
30
+ return {
31
+ isSupported,
32
+ isLoading,
33
+ };
34
+ }
@@ -1,4 +1,4 @@
1
- import { AccountTypeEnum, ChainTypeEnum, EmbeddedState } from '@openfort/openfort-js';
1
+ import { AccountTypeEnum, ChainTypeEnum, EmbeddedState, RecoveryMethod, } from '@openfort/openfort-js';
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
3
  import { useOpenfortContext } from '../../core/context';
4
4
  import { onError, onSuccess } from '../../lib/hookConsistency';
@@ -246,10 +246,19 @@ export function useEmbeddedEthereumWallet(options = {}) {
246
246
  return acc;
247
247
  }, []);
248
248
  return deduplicatedAccounts.map((account, index) => ({
249
+ id: account.id,
249
250
  address: account.address,
251
+ chainType: ChainTypeEnum.EVM,
252
+ chainId: account.chainId,
250
253
  ownerAddress: account.ownerAddress,
254
+ factoryAddress: account.factoryAddress,
255
+ salt: account.salt,
256
+ accountType: account.accountType,
257
+ implementationAddress: account.implementationAddress,
258
+ createdAt: account.createdAt,
251
259
  implementationType: account.implementationType,
252
- chainType: ChainTypeEnum.EVM,
260
+ recoveryMethod: account.recoveryMethod,
261
+ recoveryMethodDetails: account.recoveryMethodDetails,
253
262
  walletIndex: index,
254
263
  getProvider: async () => await getEthereumProvider(),
255
264
  }));
@@ -303,7 +312,10 @@ export function useEmbeddedEthereumWallet(options = {}) {
303
312
  },
304
313
  });
305
314
  if (createOptions?.onSuccess) {
306
- createOptions.onSuccess({ account: embeddedAccount, provider: ethProvider });
315
+ createOptions.onSuccess({
316
+ account: embeddedAccount,
317
+ provider: ethProvider,
318
+ });
307
319
  }
308
320
  if (options.onCreateSuccess) {
309
321
  options.onCreateSuccess(embeddedAccount, ethProvider);
@@ -382,8 +394,30 @@ export function useEmbeddedEthereumWallet(options = {}) {
382
394
  : `No embedded smart account found for address ${setActiveOptions.address} on chain ID ${chainId}`;
383
395
  throw new OpenfortError(errorMsg, OpenfortErrorType.WALLET_ERROR);
384
396
  }
397
+ // Auto-detect recovery method from account if not explicitly provided
398
+ let effectiveRecoveryMethod = setActiveOptions.recoveryMethod;
399
+ let effectivePasskeyId = setActiveOptions.passkeyId;
400
+ if (!effectiveRecoveryMethod && embeddedAccountToRecover.recoveryMethod) {
401
+ if (embeddedAccountToRecover.recoveryMethod === RecoveryMethod.PASSKEY) {
402
+ effectiveRecoveryMethod = 'passkey';
403
+ if (!effectivePasskeyId) {
404
+ const details = embeddedAccountToRecover.recoveryMethodDetails;
405
+ if (details && 'passkeyId' in details && typeof details.passkeyId === 'string') {
406
+ effectivePasskeyId = details.passkeyId;
407
+ }
408
+ }
409
+ }
410
+ else if (embeddedAccountToRecover.recoveryMethod === RecoveryMethod.PASSWORD) {
411
+ effectiveRecoveryMethod = 'password';
412
+ }
413
+ }
385
414
  // Build recovery params
386
- const recoveryParams = await buildRecoveryParams({ ...setActiveOptions, userId: user?.id }, walletConfig);
415
+ const recoveryParams = await buildRecoveryParams({
416
+ ...setActiveOptions,
417
+ userId: user?.id,
418
+ recoveryMethod: effectiveRecoveryMethod,
419
+ passkeyId: effectivePasskeyId,
420
+ }, walletConfig);
387
421
  // Recover the embedded wallet
388
422
  const embeddedAccount = await client.embeddedWallet.recover({
389
423
  account: embeddedAccountToRecover.id,
@@ -396,10 +430,19 @@ export function useEmbeddedEthereumWallet(options = {}) {
396
430
  const walletIndex = embeddedAccounts.findIndex((acc) => acc.address.toLowerCase() === embeddedAccount.address.toLowerCase() &&
397
431
  acc.chainId === embeddedAccount.chainId);
398
432
  const wallet = {
433
+ id: embeddedAccount.id,
399
434
  address: embeddedAccount.address,
435
+ chainType: ChainTypeEnum.EVM,
436
+ chainId: embeddedAccount.chainId,
400
437
  ownerAddress: embeddedAccount.ownerAddress,
438
+ factoryAddress: embeddedAccount.factoryAddress,
439
+ salt: embeddedAccount.salt,
440
+ accountType: embeddedAccount.accountType,
441
+ implementationAddress: embeddedAccount.implementationAddress,
442
+ createdAt: embeddedAccount.createdAt,
401
443
  implementationType: embeddedAccount.implementationType,
402
- chainType: ChainTypeEnum.EVM,
444
+ recoveryMethod: embeddedAccount.recoveryMethod,
445
+ recoveryMethodDetails: embeddedAccount.recoveryMethodDetails,
403
446
  walletIndex: walletIndex >= 0 ? walletIndex : 0,
404
447
  getProvider: async () => ethProvider,
405
448
  };
@@ -485,10 +528,19 @@ export function useEmbeddedEthereumWallet(options = {}) {
485
528
  // Find the wallet index in the accounts list
486
529
  const accountIndex = embeddedAccounts.findIndex((acc) => acc.id === activeWalletId);
487
530
  return {
531
+ id: activeAccount.id,
488
532
  address: activeAccount.address,
533
+ chainType: ChainTypeEnum.EVM,
534
+ chainId: activeAccount.chainId,
489
535
  ownerAddress: activeAccount.ownerAddress,
536
+ factoryAddress: activeAccount.factoryAddress,
537
+ salt: activeAccount.salt,
538
+ accountType: activeAccount.accountType,
539
+ implementationAddress: activeAccount.implementationAddress,
540
+ createdAt: activeAccount.createdAt,
490
541
  implementationType: activeAccount.implementationType,
491
- chainType: ChainTypeEnum.EVM,
542
+ recoveryMethod: activeAccount.recoveryMethod,
543
+ recoveryMethodDetails: activeAccount.recoveryMethodDetails,
492
544
  walletIndex: accountIndex >= 0 ? accountIndex : 0,
493
545
  getProvider: async () => await getEthereumProvider(),
494
546
  };
@@ -510,10 +562,19 @@ export function useEmbeddedEthereumWallet(options = {}) {
510
562
  return { ...baseActions, status: 'creating', activeWallet: null };
511
563
  }
512
564
  if (status.status === 'connecting' || status.status === 'reconnecting' || status.status === 'loading') {
513
- return { ...baseActions, status: 'connecting', activeWallet: activeWallet };
565
+ return {
566
+ ...baseActions,
567
+ status: 'connecting',
568
+ activeWallet: activeWallet,
569
+ };
514
570
  }
515
571
  if (status.status === 'error') {
516
- return { ...baseActions, status: 'error', activeWallet, error: status.error?.message || 'Unknown error' };
572
+ return {
573
+ ...baseActions,
574
+ status: 'error',
575
+ activeWallet,
576
+ error: status.error?.message || 'Unknown error',
577
+ };
517
578
  }
518
579
  // Priority 2: Check authentication state from context
519
580
  if (embeddedState !== EmbeddedState.READY && embeddedState !== EmbeddedState.CREATING_ACCOUNT) {
@@ -527,7 +588,11 @@ export function useEmbeddedEthereumWallet(options = {}) {
527
588
  }
528
589
  if (activeAccount && !provider) {
529
590
  // Have wallet but provider not initialized yet (mount recovery in progress)
530
- return { ...baseActions, status: 'connecting', activeWallet: activeWallet };
591
+ return {
592
+ ...baseActions,
593
+ status: 'connecting',
594
+ activeWallet: activeWallet,
595
+ };
531
596
  }
532
597
  // Default: disconnected (authenticated but no wallet selected)
533
598
  return { ...baseActions, status: 'disconnected', activeWallet: null };
@@ -1,4 +1,4 @@
1
- import { AccountTypeEnum, ChainTypeEnum, EmbeddedState } from '@openfort/openfort-js';
1
+ import { AccountTypeEnum, ChainTypeEnum, EmbeddedState, RecoveryMethod, } from '@openfort/openfort-js';
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
3
  import { useOpenfortContext } from '../../core/context';
4
4
  import { onError, onSuccess } from '../../lib/hookConsistency';
@@ -224,7 +224,9 @@ export function useEmbeddedSolanaWallet(options = {}) {
224
224
  },
225
225
  signMessage: async (message) => {
226
226
  // Sign message using openfort-js (with hashMessage: false for Solana)
227
- const result = await client.embeddedWallet.signMessage(message, { hashMessage: false });
227
+ const result = await client.embeddedWallet.signMessage(message, {
228
+ hashMessage: false,
229
+ });
228
230
  return result;
229
231
  },
230
232
  });
@@ -265,8 +267,12 @@ export function useEmbeddedSolanaWallet(options = {}) {
265
267
  // Build wallets list (simple deduplication by address)
266
268
  const wallets = useMemo(() => {
267
269
  return embeddedAccounts.map((account, index) => ({
270
+ id: account.id,
268
271
  address: account.address,
269
272
  chainType: ChainTypeEnum.SVM,
273
+ createdAt: account.createdAt,
274
+ recoveryMethod: account.recoveryMethod,
275
+ recoveryMethodDetails: account.recoveryMethodDetails,
270
276
  walletIndex: index,
271
277
  getProvider: async () => await getSolanaProvider(account),
272
278
  }));
@@ -278,7 +284,11 @@ export function useEmbeddedSolanaWallet(options = {}) {
278
284
  setStatus({ status: 'creating' });
279
285
  // Build recovery params (only use recoveryPassword, otpCode, and userId, ignore createAdditional)
280
286
  const recoveryParams = await buildRecoveryParams(createOptions?.recoveryPassword || createOptions?.otpCode || user?.id
281
- ? { recoveryPassword: createOptions?.recoveryPassword, otpCode: createOptions?.otpCode, userId: user?.id }
287
+ ? {
288
+ recoveryPassword: createOptions?.recoveryPassword,
289
+ otpCode: createOptions?.otpCode,
290
+ userId: user?.id,
291
+ }
282
292
  : undefined, walletConfig);
283
293
  // Create embedded wallet
284
294
  const embeddedAccount = await client.embeddedWallet.create({
@@ -303,7 +313,10 @@ export function useEmbeddedSolanaWallet(options = {}) {
303
313
  },
304
314
  });
305
315
  if (createOptions?.onSuccess) {
306
- createOptions.onSuccess({ account: embeddedAccount, provider: solProvider });
316
+ createOptions.onSuccess({
317
+ account: embeddedAccount,
318
+ provider: solProvider,
319
+ });
307
320
  }
308
321
  if (options.onCreateSuccess) {
309
322
  options.onCreateSuccess(embeddedAccount, solProvider);
@@ -357,8 +370,30 @@ export function useEmbeddedSolanaWallet(options = {}) {
357
370
  if (!embeddedAccountToRecover) {
358
371
  throw new OpenfortError(`No embedded Solana account found for address ${setActiveOptions.address}`, OpenfortErrorType.WALLET_ERROR);
359
372
  }
373
+ // Auto-detect recovery method from account if not explicitly provided
374
+ let effectiveRecoveryMethod = setActiveOptions.recoveryMethod;
375
+ let effectivePasskeyId = setActiveOptions.passkeyId;
376
+ if (!effectiveRecoveryMethod && embeddedAccountToRecover.recoveryMethod) {
377
+ if (embeddedAccountToRecover.recoveryMethod === RecoveryMethod.PASSKEY) {
378
+ effectiveRecoveryMethod = 'passkey';
379
+ if (!effectivePasskeyId) {
380
+ const details = embeddedAccountToRecover.recoveryMethodDetails;
381
+ if (details && 'passkeyId' in details && typeof details.passkeyId === 'string') {
382
+ effectivePasskeyId = details.passkeyId;
383
+ }
384
+ }
385
+ }
386
+ else if (embeddedAccountToRecover.recoveryMethod === RecoveryMethod.PASSWORD) {
387
+ effectiveRecoveryMethod = 'password';
388
+ }
389
+ }
360
390
  // Build recovery params
361
- const recoveryParams = await buildRecoveryParams({ ...setActiveOptions, userId: user?.id }, walletConfig);
391
+ const recoveryParams = await buildRecoveryParams({
392
+ ...setActiveOptions,
393
+ userId: user?.id,
394
+ recoveryMethod: effectiveRecoveryMethod,
395
+ passkeyId: effectivePasskeyId,
396
+ }, walletConfig);
362
397
  // Recover the embedded wallet
363
398
  const embeddedAccount = await client.embeddedWallet.recover({
364
399
  account: embeddedAccountToRecover.id,
@@ -370,8 +405,12 @@ export function useEmbeddedSolanaWallet(options = {}) {
370
405
  // Find the wallet index in the accounts list
371
406
  const walletIndex = embeddedAccounts.findIndex((acc) => acc.address.toLowerCase() === embeddedAccount.address.toLowerCase());
372
407
  const wallet = {
408
+ id: embeddedAccount.id,
373
409
  address: embeddedAccount.address,
374
410
  chainType: ChainTypeEnum.SVM,
411
+ createdAt: embeddedAccount.createdAt,
412
+ recoveryMethod: embeddedAccount.recoveryMethod,
413
+ recoveryMethodDetails: embeddedAccount.recoveryMethodDetails,
375
414
  walletIndex: walletIndex >= 0 ? walletIndex : 0,
376
415
  getProvider: async () => solProvider,
377
416
  };
@@ -422,8 +461,12 @@ export function useEmbeddedSolanaWallet(options = {}) {
422
461
  // Find the wallet index in the accounts list
423
462
  const accountIndex = embeddedAccounts.findIndex((acc) => acc.id === activeWalletId);
424
463
  return {
464
+ id: activeAccount.id,
425
465
  address: activeAccount.address,
426
466
  chainType: ChainTypeEnum.SVM,
467
+ createdAt: activeAccount.createdAt,
468
+ recoveryMethod: activeAccount.recoveryMethod,
469
+ recoveryMethodDetails: activeAccount.recoveryMethodDetails,
427
470
  walletIndex: accountIndex >= 0 ? accountIndex : 0,
428
471
  getProvider: async () => await getSolanaProvider(activeAccount),
429
472
  };
@@ -443,10 +486,15 @@ export function useEmbeddedSolanaWallet(options = {}) {
443
486
  return { ...baseActions, status: 'creating', activeWallet: null };
444
487
  }
445
488
  if (status.status === 'connecting' || status.status === 'reconnecting' || status.status === 'loading') {
446
- return { ...baseActions, status: 'connecting' };
489
+ return { ...baseActions, status: 'connecting', activeWallet };
447
490
  }
448
491
  if (status.status === 'error') {
449
- return { ...baseActions, status: 'error', activeWallet, error: status.error?.message || 'Unknown error' };
492
+ return {
493
+ ...baseActions,
494
+ status: 'error',
495
+ activeWallet,
496
+ error: status.error?.message || 'Unknown error',
497
+ };
450
498
  }
451
499
  // Priority 2: Check authentication state from context
452
500
  if (embeddedState !== EmbeddedState.READY && embeddedState !== EmbeddedState.CREATING_ACCOUNT) {
@@ -460,7 +508,7 @@ export function useEmbeddedSolanaWallet(options = {}) {
460
508
  }
461
509
  if (activeAccount && !provider) {
462
510
  // Have wallet but provider not initialized yet (mount recovery in progress)
463
- return { ...baseActions, status: 'connecting' };
511
+ return { ...baseActions, status: 'connecting', activeWallet };
464
512
  }
465
513
  // Default: disconnected (authenticated but no wallet selected)
466
514
  return { ...baseActions, status: 'disconnected', activeWallet: null };
@@ -65,12 +65,33 @@ async function resolveEncryptionSession(walletConfig, otpCode, userId) {
65
65
  * @internal
66
66
  */
67
67
  export async function buildRecoveryParams(options, walletConfig) {
68
- if (options?.recoveryPassword) {
68
+ // If passkey recovery method is explicitly requested
69
+ if (options?.recoveryMethod === 'passkey') {
70
+ // If passkeyId is provided, use it for recovery
71
+ if (options.passkeyId) {
72
+ return {
73
+ recoveryMethod: RecoveryMethod.PASSKEY,
74
+ passkeyInfo: {
75
+ passkeyId: options.passkeyId,
76
+ },
77
+ };
78
+ }
79
+ // If no passkeyId, this is a creation request - SDK will create the passkey
80
+ return {
81
+ recoveryMethod: RecoveryMethod.PASSKEY,
82
+ };
83
+ }
84
+ // If password recovery method is explicitly requested or password is provided
85
+ if (options?.recoveryMethod === 'password' || options?.recoveryPassword) {
86
+ if (!options?.recoveryPassword) {
87
+ throw new OpenfortError('Recovery password is required when using password recovery method', OpenfortErrorType.WALLET_ERROR);
88
+ }
69
89
  return {
70
90
  recoveryMethod: RecoveryMethod.PASSWORD,
71
91
  password: options.recoveryPassword,
72
92
  };
73
93
  }
94
+ // Default to automatic recovery
74
95
  return {
75
96
  recoveryMethod: RecoveryMethod.AUTOMATIC,
76
97
  encryptionSession: await resolveEncryptionSession(walletConfig, options?.otpCode, options?.userId),
@@ -1,6 +1,7 @@
1
- // WebView integration
2
1
  // OAuth flows
3
2
  export { authenticateWithApple, createOAuthRedirectUri, isAppleSignInAvailable, OAuthUtils, openOAuthSession, parseOAuthUrl, } from './oauth';
3
+ // Passkey handler and support checks
4
+ export { getPasskeyDiagnostics, isPasskeySupported, NativePasskeyHandler } from './passkey';
4
5
  // Storage utilities
5
6
  export { handleSecureStorageMessage, isSecureStorageMessage, NativeStorageUtils, } from './storage';
6
7
  export { EmbeddedWalletWebView, WebViewUtils } from './webview';
@@ -0,0 +1,316 @@
1
+ import { PasskeyAssertionFailedError, PasskeyCreationFailedError, PasskeyPRFNotSupportedError, PasskeySeedInvalidError, PasskeyUserCancelledError, } from '@openfort/openfort-js';
2
+ import { logger } from '../lib/logger';
3
+ /**
4
+ * Utility functions for passkey operations in React Native.
5
+ * Handles base64/base64url encoding, key extraction, and challenge generation.
6
+ */
7
+ const PasskeyUtils = {
8
+ /** Valid byte lengths for derived keys (AES-128, AES-192, AES-256) */
9
+ validByteLengths: [16, 24, 32],
10
+ /**
11
+ * Validates that the key byte length is valid for AES encryption.
12
+ * @throws Error if length is not 16, 24, or 32
13
+ */
14
+ validateKeyByteLength(length) {
15
+ if (!this.validByteLengths.includes(length)) {
16
+ throw new Error(`Invalid key byte length ${length}. Must be 16, 24, or 32.`);
17
+ }
18
+ },
19
+ /**
20
+ * Generates a random 32-byte challenge for WebAuthn operations.
21
+ */
22
+ generateChallenge() {
23
+ const challenge = new Uint8Array(32);
24
+ if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
25
+ crypto.getRandomValues(challenge);
26
+ }
27
+ else {
28
+ // Fallback for environments without crypto.getRandomValues
29
+ for (let i = 0; i < 32; i++) {
30
+ challenge[i] = Math.floor(Math.random() * 256);
31
+ }
32
+ }
33
+ return challenge;
34
+ },
35
+ /**
36
+ * Converts ArrayBuffer or Uint8Array to base64url string.
37
+ * Base64URL uses '-' and '_' instead of '+' and '/', and omits padding '='.
38
+ */
39
+ arrayBufferToBase64URL(buffer) {
40
+ const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
41
+ let binary = '';
42
+ for (const byte of bytes) {
43
+ binary += String.fromCharCode(byte);
44
+ }
45
+ const base64 = btoa(binary);
46
+ return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
47
+ },
48
+ /**
49
+ * Converts base64url string to Uint8Array.
50
+ */
51
+ base64URLToUint8Array(base64url) {
52
+ // Convert base64url to base64
53
+ let base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
54
+ // Add padding if needed
55
+ while (base64.length % 4) {
56
+ base64 += '=';
57
+ }
58
+ const binary = atob(base64);
59
+ const bytes = new Uint8Array(binary.length);
60
+ for (let i = 0; i < binary.length; i++) {
61
+ bytes[i] = binary.charCodeAt(i);
62
+ }
63
+ return bytes;
64
+ },
65
+ /**
66
+ * Converts standard base64 to base64url format.
67
+ * This is idempotent - safe to call on strings already in base64url format.
68
+ */
69
+ base64ToBase64URL(base64) {
70
+ return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
71
+ },
72
+ /**
73
+ * Extracts the first N bytes from a PRF result for use as key material.
74
+ */
75
+ extractRawKeyBytes(prfResult, length) {
76
+ return prfResult.slice(0, length);
77
+ },
78
+ };
79
+ let passkeysModule = null;
80
+ let passkeysLoadAttempted = false;
81
+ let passkeysLoadError = null;
82
+ /**
83
+ * Returns the passkeys API (create, get, isSupported). Resolves module.Passkeys ?? module once.
84
+ * Returns null if the module failed to load.
85
+ */
86
+ function getPasskeysAPI() {
87
+ if (passkeysLoadAttempted) {
88
+ return passkeysLoadError ? null : passkeysModule ? (passkeysModule.Passkeys ?? passkeysModule) : null;
89
+ }
90
+ passkeysLoadAttempted = true;
91
+ try {
92
+ passkeysModule = require('react-native-passkeys');
93
+ return passkeysModule ? (passkeysModule.Passkeys ?? passkeysModule) : null;
94
+ }
95
+ catch (error) {
96
+ passkeysLoadError = error instanceof Error ? error : new Error(String(error));
97
+ return null;
98
+ }
99
+ }
100
+ /**
101
+ * Returns diagnostic information about passkey support.
102
+ * Useful for debugging why passkeys may not be available.
103
+ */
104
+ export function getPasskeyDiagnostics() {
105
+ const api = getPasskeysAPI();
106
+ return {
107
+ isSupported: api !== null && api.isSupported !== undefined,
108
+ loadError: passkeysLoadError,
109
+ moduleLoaded: passkeysLoadAttempted && passkeysLoadError === null,
110
+ };
111
+ }
112
+ /**
113
+ * Checks if the device supports passkeys (WebAuthn). Uses the library's isSupported() only — no credential creation.
114
+ * Normalizes sync/async and function/boolean from react-native-passkeys.
115
+ */
116
+ export async function isPasskeySupported() {
117
+ const api = getPasskeysAPI();
118
+ if (!api || api.isSupported == null) {
119
+ return false;
120
+ }
121
+ const supported = api.isSupported;
122
+ if (typeof supported === 'boolean') {
123
+ return supported;
124
+ }
125
+ if (typeof supported === 'function') {
126
+ const result = supported();
127
+ return result instanceof Promise ? result : Promise.resolve(result);
128
+ }
129
+ return false;
130
+ }
131
+ /**
132
+ * NativePasskeyHandler implements IPasskeyHandler using react-native-passkeys (create/get)
133
+ * as the native equivalent of navigator.credentials.create/get. Same contract as openfort-js
134
+ * PasskeyHandler; key is returned to the SDK/Shield like on web.
135
+ */
136
+ export class NativePasskeyHandler {
137
+ rpId;
138
+ rpName;
139
+ timeout;
140
+ derivedKeyLengthBytes;
141
+ constructor(config) {
142
+ this.rpId = config.rpId;
143
+ this.rpName = config.rpName;
144
+ this.timeout = config.timeout ?? 60_000;
145
+ this.derivedKeyLengthBytes = config.derivedKeyLengthBytes ?? 32;
146
+ PasskeyUtils.validateKeyByteLength(this.derivedKeyLengthBytes);
147
+ }
148
+ /**
149
+ * Normalizes prf.results.first from the native module to Uint8Array.
150
+ * On Android, the bridge may return base64 string or array of numbers.
151
+ */
152
+ normalizePRFResult(first) {
153
+ if (typeof first === 'string') {
154
+ return PasskeyUtils.base64URLToUint8Array(first);
155
+ }
156
+ if (first instanceof ArrayBuffer) {
157
+ return new Uint8Array(first);
158
+ }
159
+ if (ArrayBuffer.isView(first)) {
160
+ return new Uint8Array(first.buffer, first.byteOffset, first.byteLength);
161
+ }
162
+ if (Array.isArray(first) || (typeof first === 'object' && first !== null && 'length' in first)) {
163
+ return new Uint8Array(first);
164
+ }
165
+ throw new Error('PRF result: expected base64 string, ArrayBuffer, TypedArray, or array of numbers');
166
+ }
167
+ /**
168
+ * Extracts key bytes from PRF result and returns as base64url string.
169
+ * Validates that the PRF result has sufficient entropy for the requested key length.
170
+ */
171
+ extractKeyBytes(prfResultBytes) {
172
+ // Validate PRF result has sufficient entropy
173
+ if (prfResultBytes.length < this.derivedKeyLengthBytes) {
174
+ throw new Error(`PRF result too short: got ${prfResultBytes.length} bytes, need at least ${this.derivedKeyLengthBytes} bytes`);
175
+ }
176
+ const keyBytes = PasskeyUtils.extractRawKeyBytes(prfResultBytes, this.derivedKeyLengthBytes);
177
+ return PasskeyUtils.arrayBufferToBase64URL(keyBytes);
178
+ }
179
+ /**
180
+ * Creates a passkey and derives a key using the PRF extension.
181
+ */
182
+ async createPasskey(config) {
183
+ if (!this.rpId || !this.rpName) {
184
+ throw new Error('rpId and rpName must be configured');
185
+ }
186
+ // Validate seed is non-empty for PRF entropy
187
+ if (!config.seed || config.seed.trim().length === 0) {
188
+ throw new PasskeySeedInvalidError();
189
+ }
190
+ const challenge = PasskeyUtils.generateChallenge();
191
+ // Android Credentials API requires base64url for challenge
192
+ const challengeBase64URL = PasskeyUtils.arrayBufferToBase64URL(challenge);
193
+ const userIdBytes = new TextEncoder().encode(config.id);
194
+ // Android Credentials API requires base64url for user.id
195
+ const userIdBase64URL = PasskeyUtils.arrayBufferToBase64URL(userIdBytes);
196
+ const publicKey = {
197
+ challenge: challengeBase64URL,
198
+ rp: { id: this.rpId, name: this.rpName },
199
+ user: {
200
+ id: userIdBase64URL,
201
+ name: config.id,
202
+ displayName: config.displayName,
203
+ },
204
+ pubKeyCredParams: [
205
+ { type: 'public-key', alg: -7 },
206
+ { type: 'public-key', alg: -257 },
207
+ ],
208
+ authenticatorSelection: {
209
+ residentKey: 'required',
210
+ userVerification: 'required',
211
+ },
212
+ excludeCredentials: [],
213
+ // PRF extension: react-native-passkeys expects all inputs as base64url
214
+ extensions: {
215
+ prf: {
216
+ eval: {
217
+ first: PasskeyUtils.arrayBufferToBase64URL(new TextEncoder().encode(config.seed)),
218
+ },
219
+ },
220
+ },
221
+ timeout: this.timeout,
222
+ attestation: 'none',
223
+ };
224
+ const api = getPasskeysAPI();
225
+ if (!api?.create || typeof api.create !== 'function') {
226
+ throw new Error('react-native-passkeys module not available');
227
+ }
228
+ let credential;
229
+ try {
230
+ credential = await api.create(publicKey);
231
+ }
232
+ catch (e) {
233
+ // Re-throw known error types
234
+ if (e instanceof PasskeyUserCancelledError)
235
+ throw e;
236
+ if (e instanceof PasskeySeedInvalidError)
237
+ throw e;
238
+ throw new PasskeyCreationFailedError(e instanceof Error ? e.message : 'Unknown error', e instanceof Error ? e : undefined);
239
+ }
240
+ if (!credential) {
241
+ // Null result typically indicates user cancellation
242
+ throw new PasskeyUserCancelledError();
243
+ }
244
+ const prfResults = credential.clientExtensionResults?.prf;
245
+ if (!prfResults?.results?.first) {
246
+ // Log warning about orphaned passkey credential
247
+ logger.warn('Passkey created but PRF extension failed. ' +
248
+ 'A passkey credential may exist on the device that cannot be used for wallet recovery. ' +
249
+ `Credential ID: ${credential.id}`);
250
+ throw new PasskeyPRFNotSupportedError();
251
+ }
252
+ const prfResultBytes = this.normalizePRFResult(prfResults.results.first);
253
+ const key = this.extractKeyBytes(prfResultBytes);
254
+ return {
255
+ id: credential.id,
256
+ displayName: config.displayName,
257
+ key,
258
+ };
259
+ }
260
+ /**
261
+ * Derives and exports key material from an existing passkey as base64url string.
262
+ */
263
+ async deriveAndExportKey(config) {
264
+ if (!this.rpId) {
265
+ throw new Error('rpId must be configured');
266
+ }
267
+ // Validate seed is non-empty for PRF entropy
268
+ if (!config.seed || config.seed.trim().length === 0) {
269
+ throw new PasskeySeedInvalidError();
270
+ }
271
+ const challenge = PasskeyUtils.generateChallenge();
272
+ const challengeBase64URL = PasskeyUtils.arrayBufferToBase64URL(challenge);
273
+ // Always normalize to base64url - this is idempotent for strings already in base64url format
274
+ const credentialId = PasskeyUtils.base64ToBase64URL(config.id);
275
+ const publicKey = {
276
+ challenge: challengeBase64URL,
277
+ rpId: this.rpId,
278
+ allowCredentials: [{ id: credentialId, type: 'public-key' }],
279
+ userVerification: 'required',
280
+ extensions: {
281
+ prf: {
282
+ eval: {
283
+ first: PasskeyUtils.arrayBufferToBase64URL(new TextEncoder().encode(config.seed)),
284
+ },
285
+ },
286
+ },
287
+ timeout: this.timeout,
288
+ };
289
+ const api = getPasskeysAPI();
290
+ if (!api?.get || typeof api.get !== 'function') {
291
+ throw new Error('react-native-passkeys module not available');
292
+ }
293
+ let assertion;
294
+ try {
295
+ assertion = await api.get(publicKey);
296
+ }
297
+ catch (e) {
298
+ // Re-throw known error types
299
+ if (e instanceof PasskeyUserCancelledError)
300
+ throw e;
301
+ if (e instanceof PasskeySeedInvalidError)
302
+ throw e;
303
+ throw new PasskeyAssertionFailedError(e instanceof Error ? e.message : 'Unknown error', e instanceof Error ? e : undefined);
304
+ }
305
+ if (!assertion) {
306
+ // Null result typically indicates user cancellation
307
+ throw new PasskeyUserCancelledError();
308
+ }
309
+ const prfResults = assertion.clientExtensionResults?.prf;
310
+ if (!prfResults?.results?.first) {
311
+ throw new PasskeyPRFNotSupportedError();
312
+ }
313
+ const prfResultBytes = this.normalizePRFResult(prfResults.results.first);
314
+ return this.extractKeyBytes(prfResultBytes);
315
+ }
316
+ }
@@ -12,7 +12,7 @@ import { handleSecureStorageMessage, isSecureStorageMessage } from './storage';
12
12
  *
13
13
  * @param props - Component props, see {@link EmbeddedWalletWebViewProps}
14
14
  */
15
- export const EmbeddedWalletWebView = ({ client, isClientReady, onProxyStatusChange, }) => {
15
+ export const EmbeddedWalletWebView = ({ client, onProxyStatusChange, debug }) => {
16
16
  const webViewRef = useRef(null);
17
17
  // Handle app state changes to monitor WebView health
18
18
  useEffect(() => {
@@ -44,7 +44,6 @@ export const EmbeddedWalletWebView = ({ client, isClientReady, onProxyStatusChan
44
44
  // Set up WebView reference with client immediately when both are available
45
45
  useEffect(() => {
46
46
  if (webViewRef.current) {
47
- // Message poster with Uint8Array preprocessing for React Native
48
47
  const messagePoster = {
49
48
  postMessage: (message) => {
50
49
  webViewRef.current?.postMessage(message);
@@ -52,7 +51,7 @@ export const EmbeddedWalletWebView = ({ client, isClientReady, onProxyStatusChan
52
51
  };
53
52
  client.embeddedWallet.setMessagePoster(messagePoster);
54
53
  }
55
- }, [client, isClientReady]);
54
+ }, [client]);
56
55
  // Clean message handler using the new penpal bridge
57
56
  const handleMessage = useCallback(async (event) => {
58
57
  try {
@@ -91,7 +90,9 @@ export const EmbeddedWalletWebView = ({ client, isClientReady, onProxyStatusChan
91
90
  return (React.createElement(View, { style: { width: 0, height: 0, overflow: 'hidden' } },
92
91
  React.createElement(WebView, { ref: handleWebViewRef, source: {
93
92
  uri: client.embeddedWallet.getURL(),
94
- }, webviewDebuggingEnabled: true, cacheEnabled: false, injectedJavaScriptObject: { shouldUseAppBackedStorage: true }, cacheMode: "LOAD_NO_CACHE", onLoad: handleLoad, onError: handleError, onMessage: handleMessage })));
93
+ },
94
+ // Enable debugging when explicitly enabled via walletConfig.debug
95
+ webviewDebuggingEnabled: debug, cacheEnabled: false, injectedJavaScriptObject: { shouldUseAppBackedStorage: true }, cacheMode: "LOAD_NO_CACHE", onLoad: handleLoad, onError: handleError, onMessage: handleMessage })));
95
96
  };
96
97
  /**
97
98
  * Utilities for WebView integration
@@ -8,8 +8,12 @@ export type CommonEmbeddedWalletConfiguration = {
8
8
  ethereumProviderPolicyId?: PolicyConfig;
9
9
  accountType?: AccountTypeEnum;
10
10
  debug?: boolean;
11
- /** Recovery method for the embedded wallet: 'automatic' or 'password' */
12
- recoveryMethod?: 'automatic' | 'password';
11
+ /** Recovery method for the embedded wallet: 'automatic', 'password', or 'passkey' */
12
+ recoveryMethod?: 'automatic' | 'password' | 'passkey';
13
+ /** Passkey Relying Party ID (domain) for passkey-based recovery */
14
+ passkeyRpId?: string;
15
+ /** Passkey Relying Party Name for passkey-based recovery */
16
+ passkeyRpName?: string;
13
17
  };
14
18
  /**
15
19
  * Parameters passed to the encryption session callback
@@ -5,4 +5,5 @@
5
5
  */
6
6
  export { useOpenfort } from './useOpenfort';
7
7
  export { useOpenfortClient } from './useOpenfortClient';
8
+ export { usePasskeySupport } from './usePasskeySupport';
8
9
  export { useUser } from './useUser';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Hook to detect if the platform supports passkeys (WebAuthn).
3
+ *
4
+ * Note: This only checks basic passkey support, not PRF extension support.
5
+ * PRF support can only be determined during passkey creation via the
6
+ * `clientExtensionResults.prf.enabled` field in the response.
7
+ *
8
+ * @returns Object with `isSupported` boolean and `isLoading` state
9
+ */
10
+ export declare function usePasskeySupport(): {
11
+ isSupported: boolean;
12
+ isLoading: boolean;
13
+ };
@@ -16,4 +16,6 @@ export declare function buildRecoveryParams(options: {
16
16
  recoveryPassword?: string;
17
17
  otpCode?: string;
18
18
  userId?: string;
19
+ recoveryMethod?: 'automatic' | 'password' | 'passkey';
20
+ passkeyId?: string;
19
21
  } | undefined, walletConfig?: EmbeddedWalletConfiguration): Promise<RecoveryParams>;
@@ -14,3 +14,4 @@ export * from './core';
14
14
  export * from './hooks';
15
15
  export * from './native';
16
16
  export * from './types';
17
+ export type { RecoveryMethodDetails } from './types/wallet';
@@ -1 +1,2 @@
1
- export {};
1
+ // Passkey error types (re-exported from @openfort/openfort-js)
2
+ export { PASSKEY_ERROR_CODES, PasskeyAssertionFailedError, PasskeyCreationFailedError, PasskeyPRFNotSupportedError, PasskeySeedInvalidError, PasskeyUserCancelledError, } from '@openfort/openfort-js';
@@ -1,5 +1,7 @@
1
1
  export type { AppleAuthResult, OAuthResult, OAuthSessionConfig, } from './oauth';
2
2
  export { authenticateWithApple, createOAuthRedirectUri, isAppleSignInAvailable, OAuthUtils, openOAuthSession, parseOAuthUrl, } from './oauth';
3
+ export type { NativePasskeyHandlerConfig, PasskeysAPI } from './passkey';
4
+ export { getPasskeyDiagnostics, isPasskeySupported, NativePasskeyHandler } from './passkey';
3
5
  export type { SecureStorageMessage, SecureStorageResponse, } from './storage';
4
6
  export { handleSecureStorageMessage, isSecureStorageMessage, NativeStorageUtils, } from './storage';
5
7
  export { EmbeddedWalletWebView, WebViewUtils } from './webview';
@@ -0,0 +1,155 @@
1
+ import { type IPasskeyHandler } from '@openfort/openfort-js';
2
+ /**
3
+ * Result from passkey credential creation.
4
+ */
5
+ interface PasskeyCredentialResult {
6
+ id: string;
7
+ rawId?: string;
8
+ type: string;
9
+ clientExtensionResults?: {
10
+ prf?: {
11
+ results?: {
12
+ first?: unknown;
13
+ };
14
+ };
15
+ };
16
+ response?: {
17
+ attestationObject?: string;
18
+ clientDataJSON?: string;
19
+ };
20
+ }
21
+ /**
22
+ * Result from passkey assertion (get).
23
+ */
24
+ interface PasskeyAssertionResult {
25
+ id: string;
26
+ type: string;
27
+ clientExtensionResults?: {
28
+ prf?: {
29
+ results?: {
30
+ first?: unknown;
31
+ };
32
+ };
33
+ };
34
+ }
35
+ /** Resolved API from react-native-passkeys (module.Passkeys ?? module). Library may export sync or async isSupported. */
36
+ export type PasskeysAPI = {
37
+ create?: (options: PublicKeyCredentialCreationOptions) => Promise<PasskeyCredentialResult | null>;
38
+ get?: (options: PublicKeyCredentialRequestOptions) => Promise<PasskeyAssertionResult | null>;
39
+ /** Sync on native (iOS/Android), sync on web; may be function or boolean. */
40
+ isSupported?: (() => boolean) | (() => Promise<boolean>) | boolean;
41
+ };
42
+ /**
43
+ * Returns diagnostic information about passkey support.
44
+ * Useful for debugging why passkeys may not be available.
45
+ */
46
+ export declare function getPasskeyDiagnostics(): {
47
+ isSupported: boolean;
48
+ loadError: Error | null;
49
+ moduleLoaded: boolean;
50
+ };
51
+ /**
52
+ * Checks if the device supports passkeys (WebAuthn). Uses the library's isSupported() only — no credential creation.
53
+ * Normalizes sync/async and function/boolean from react-native-passkeys.
54
+ */
55
+ export declare function isPasskeySupported(): Promise<boolean>;
56
+ export interface NativePasskeyHandlerConfig {
57
+ rpId?: string;
58
+ rpName?: string;
59
+ timeout?: number;
60
+ derivedKeyLengthBytes?: number;
61
+ }
62
+ interface PublicKeyCredentialCreationOptions {
63
+ challenge: string;
64
+ rp: {
65
+ id: string;
66
+ name: string;
67
+ };
68
+ user: {
69
+ id: string;
70
+ name: string;
71
+ displayName: string;
72
+ };
73
+ pubKeyCredParams: Array<{
74
+ type: string;
75
+ alg: number;
76
+ }>;
77
+ authenticatorSelection: {
78
+ authenticatorAttachment?: string;
79
+ residentKey?: string;
80
+ requireResidentKey?: boolean;
81
+ userVerification?: string;
82
+ };
83
+ excludeCredentials?: Array<{
84
+ id: string;
85
+ type: string;
86
+ }>;
87
+ extensions?: {
88
+ prf?: {
89
+ eval?: {
90
+ first: string;
91
+ };
92
+ };
93
+ };
94
+ timeout?: number;
95
+ attestation?: string;
96
+ }
97
+ interface PublicKeyCredentialRequestOptions {
98
+ challenge: string;
99
+ rpId: string;
100
+ allowCredentials: Array<{
101
+ id: string;
102
+ type: string;
103
+ }>;
104
+ userVerification: string;
105
+ extensions?: {
106
+ prf?: {
107
+ eval?: {
108
+ first: string;
109
+ };
110
+ };
111
+ };
112
+ timeout?: number;
113
+ }
114
+ /**
115
+ * NativePasskeyHandler implements IPasskeyHandler using react-native-passkeys (create/get)
116
+ * as the native equivalent of navigator.credentials.create/get. Same contract as openfort-js
117
+ * PasskeyHandler; key is returned to the SDK/Shield like on web.
118
+ */
119
+ export declare class NativePasskeyHandler implements IPasskeyHandler {
120
+ private readonly rpId?;
121
+ private readonly rpName?;
122
+ private readonly timeout;
123
+ private readonly derivedKeyLengthBytes;
124
+ constructor(config: NativePasskeyHandlerConfig);
125
+ /**
126
+ * Normalizes prf.results.first from the native module to Uint8Array.
127
+ * On Android, the bridge may return base64 string or array of numbers.
128
+ */
129
+ private normalizePRFResult;
130
+ /**
131
+ * Extracts key bytes from PRF result and returns as base64url string.
132
+ * Validates that the PRF result has sufficient entropy for the requested key length.
133
+ */
134
+ private extractKeyBytes;
135
+ /**
136
+ * Creates a passkey and derives a key using the PRF extension.
137
+ */
138
+ createPasskey(config: {
139
+ id: string;
140
+ displayName: string;
141
+ seed: string;
142
+ }): Promise<{
143
+ id: string;
144
+ displayName?: string;
145
+ key?: string;
146
+ }>;
147
+ /**
148
+ * Derives and exports key material from an existing passkey as base64url string.
149
+ */
150
+ deriveAndExportKey(config: {
151
+ id: string;
152
+ seed: string;
153
+ }): Promise<string>;
154
+ }
155
+ export {};
@@ -11,6 +11,8 @@ interface EmbeddedWalletWebViewProps {
11
11
  isClientReady: boolean;
12
12
  /** Callback when WebView proxy status changes */
13
13
  onProxyStatusChange?: (status: 'loading' | 'loaded' | 'reloading') => void;
14
+ /** Enable WebView debugging (allows inspection via Safari/Chrome dev tools) */
15
+ debug?: boolean;
14
16
  }
15
17
  /**
16
18
  * WebView component for embedded wallet integration
@@ -6,6 +6,7 @@ export interface UseOpenfort {
6
6
  /** Any error encountered during SDK initialization. */
7
7
  error: Error | null;
8
8
  }
9
+ export { PASSKEY_ERROR_CODES, PasskeyAssertionFailedError, PasskeyCreationFailedError, type PasskeyErrorCode, PasskeyPRFNotSupportedError, PasskeySeedInvalidError, PasskeyUserCancelledError, } from '@openfort/openfort-js';
9
10
  export type { AuthSuccessCallback, EmailLoginHookOptions, EmailLoginHookResult, ErrorCallback, GenerateSiweMessage, GenerateSiweMessageResponse, PasswordFlowState, RecoveryFlowState, SiweFlowState, SiweLoginHookOptions, SiweLoginHookResult, } from './auth';
10
11
  export type { LinkWithOAuthInput, LoginWithOAuthInput, OAuthFlowState, UseLoginWithOAuth, } from './oauth';
11
12
  export type { ConnectedEmbeddedEthereumWallet, ConnectedEmbeddedSolanaWallet, CreateEthereumWalletOptions, CreateEthereumWalletResult, CreateSolanaEmbeddedWalletOpts, CreateSolanaWalletOptions, CreateSolanaWalletResult, EIP1193EventHandler, EIP1193EventName, EIP1193RequestArguments, EmbeddedEthereumWalletState, EmbeddedSolanaWalletState, EthereumWalletActions, OpenfortEmbeddedEthereumWalletProvider, OpenfortEmbeddedSolanaWalletProvider, SetActiveEthereumWalletOptions, SetActiveEthereumWalletResult, SetActiveSolanaWalletOptions, SetActiveSolanaWalletResult, SetRecoveryOptions, SetRecoveryResult, SignedSolanaTransaction, SolanaRequestArguments, SolanaSignMessageRequest, SolanaSignTransactionRequest, SolanaTransaction, SolanaWalletActions, } from './wallet';
@@ -1,4 +1,8 @@
1
- import type { AccountTypeEnum, ChainTypeEnum, EmbeddedAccount, RecoveryParams } from '@openfort/openfort-js';
1
+ import type { AccountTypeEnum, ChainTypeEnum, EmbeddedAccount, RecoveryMethod, RecoveryParams } from '@openfort/openfort-js';
2
+ /**
3
+ * Recovery method details extracted from EmbeddedAccount
4
+ */
5
+ export type RecoveryMethodDetails = EmbeddedAccount['recoveryMethodDetails'];
2
6
  import type { Hex } from './hex';
3
7
  import type { OpenfortHookOptions } from './hookOption';
4
8
  import type { OpenfortError } from './openfortError';
@@ -105,20 +109,56 @@ export interface OpenfortEmbeddedSolanaWalletProvider {
105
109
  * Connected Ethereum wallet
106
110
  */
107
111
  export type ConnectedEmbeddedEthereumWallet = {
112
+ /** Account ID */
113
+ id: string;
114
+ /** Account address */
108
115
  address: string;
116
+ /** Chain type (always EVM) */
117
+ chainType: ChainTypeEnum.EVM;
118
+ /** Chain ID */
119
+ chainId?: number;
120
+ /** Owner address (for smart accounts) */
109
121
  ownerAddress?: string;
122
+ /** Factory address (for smart accounts) */
123
+ factoryAddress?: string;
124
+ /** Salt (for smart accounts) */
125
+ salt?: string;
126
+ /** Account type (EOA, Smart Account, Delegated) */
127
+ accountType: AccountTypeEnum;
128
+ /** Implementation address (for smart accounts) */
129
+ implementationAddress?: string;
130
+ /** Creation timestamp */
131
+ createdAt?: number;
132
+ /** Implementation type */
110
133
  implementationType?: string;
111
- chainType: ChainTypeEnum.EVM;
134
+ /** Recovery method used for this wallet */
135
+ recoveryMethod?: RecoveryMethod;
136
+ /** Recovery method details (e.g., passkey info) */
137
+ recoveryMethodDetails?: RecoveryMethodDetails;
138
+ /** Index in the wallets array */
112
139
  walletIndex: number;
140
+ /** Get the EIP-1193 provider for this wallet */
113
141
  getProvider: () => Promise<OpenfortEmbeddedEthereumWalletProvider>;
114
142
  };
115
143
  /**
116
144
  * Connected Solana wallet
117
145
  */
118
146
  export type ConnectedEmbeddedSolanaWallet = {
147
+ /** Account ID */
148
+ id: string;
149
+ /** Account address (public key) */
119
150
  address: string;
151
+ /** Chain type (always SVM) */
120
152
  chainType: ChainTypeEnum.SVM;
153
+ /** Creation timestamp */
154
+ createdAt?: number;
155
+ /** Recovery method used for this wallet */
156
+ recoveryMethod?: RecoveryMethod;
157
+ /** Recovery method details (e.g., passkey info) */
158
+ recoveryMethodDetails?: RecoveryMethodDetails;
159
+ /** Index in the wallets array */
121
160
  walletIndex: number;
161
+ /** Get the Solana provider for this wallet */
122
162
  getProvider: () => Promise<OpenfortEmbeddedSolanaWalletProvider>;
123
163
  };
124
164
  /**
@@ -139,6 +179,10 @@ export type CreateEthereumWalletOptions = {
139
179
  otpCode?: string;
140
180
  accountType?: AccountTypeEnum;
141
181
  policyId?: string;
182
+ /** Recovery method to use: 'automatic', 'password', or 'passkey' */
183
+ recoveryMethod?: 'automatic' | 'password' | 'passkey';
184
+ /** Passkey ID for passkey recovery (required when recoveryMethod is 'passkey' for recovery) */
185
+ passkeyId?: string;
142
186
  } & OpenfortHookOptions<CreateEthereumWalletResult>;
143
187
  /**
144
188
  * Result of setting active Ethereum wallet
@@ -157,6 +201,10 @@ export type SetActiveEthereumWalletOptions = {
157
201
  recoveryPassword?: string;
158
202
  /** OTP code for Shield verification when using automatic recovery */
159
203
  otpCode?: string;
204
+ /** Recovery method to use: 'automatic', 'password', or 'passkey' */
205
+ recoveryMethod?: 'automatic' | 'password' | 'passkey';
206
+ /** Passkey ID for passkey recovery (required when recoveryMethod is 'passkey' for recovery) */
207
+ passkeyId?: string;
160
208
  } & OpenfortHookOptions<SetActiveEthereumWalletResult>;
161
209
  /**
162
210
  * Result of setting recovery method
@@ -188,6 +236,10 @@ export type CreateSolanaEmbeddedWalletOpts = {
188
236
  * Create additional wallet if one already exists
189
237
  */
190
238
  createAdditional?: boolean;
239
+ /** Recovery method to use: 'automatic', 'password', or 'passkey' */
240
+ recoveryMethod?: 'automatic' | 'password' | 'passkey';
241
+ /** Passkey ID for passkey recovery (required when recoveryMethod is 'passkey' for recovery) */
242
+ passkeyId?: string;
191
243
  };
192
244
  /**
193
245
  * Result of creating a Solana wallet
@@ -217,6 +269,10 @@ export type SetActiveSolanaWalletOptions = {
217
269
  recoveryPassword?: string;
218
270
  /** OTP code for Shield verification when using automatic recovery */
219
271
  otpCode?: string;
272
+ /** Recovery method to use: 'automatic', 'password', or 'passkey' */
273
+ recoveryMethod?: 'automatic' | 'password' | 'passkey';
274
+ /** Passkey ID for passkey recovery (required when recoveryMethod is 'passkey' for recovery) */
275
+ passkeyId?: string;
220
276
  } & OpenfortHookOptions<SetActiveSolanaWalletResult>;
221
277
  /**
222
278
  * Common actions available on all Ethereum wallet states
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@openfort/react-native",
3
3
  "main": "dist/index.js",
4
- "version": "1.0.5",
4
+ "version": "1.0.6",
5
5
  "license": "MIT",
6
6
  "description": "React Native SDK for Openfort platform integration",
7
7
  "repository": {
@@ -24,7 +24,8 @@
24
24
  }
25
25
  },
26
26
  "dependencies": {
27
- "@openfort/openfort-js": "^1.1.4"
27
+ "@openfort/openfort-js": "^1.1.5",
28
+ "react-native-passkeys": "0.4.0"
28
29
  },
29
30
  "peerDependencies": {
30
31
  "expo-apple-authentication": "*",
@@ -77,7 +78,10 @@
77
78
  "size-limit": [
78
79
  {
79
80
  "path": "dist/index.js",
80
- "limit": "250 KB"
81
+ "limit": "250 KB",
82
+ "ignore": [
83
+ "expo-modules-core"
84
+ ]
81
85
  }
82
86
  ],
83
87
  "scripts": {