@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.
- package/dist/core/provider.js +23 -5
- package/dist/hooks/core/index.js +1 -0
- package/dist/hooks/core/usePasskeySupport.js +34 -0
- package/dist/hooks/wallet/useEmbeddedEthereumWallet.js +74 -9
- package/dist/hooks/wallet/useEmbeddedSolanaWallet.js +56 -8
- package/dist/hooks/wallet/utils.js +22 -1
- package/dist/native/index.js +2 -1
- package/dist/native/passkey.js +316 -0
- package/dist/native/webview.js +5 -4
- package/dist/types/core/provider.d.ts +6 -2
- package/dist/types/hooks/core/index.d.ts +1 -0
- package/dist/types/hooks/core/usePasskeySupport.d.ts +13 -0
- package/dist/types/hooks/wallet/utils.d.ts +2 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +2 -1
- package/dist/types/native/index.d.ts +2 -0
- package/dist/types/native/passkey.d.ts +155 -0
- package/dist/types/native/webview.d.ts +2 -0
- package/dist/types/types/index.d.ts +1 -0
- package/dist/types/types/wallet.d.ts +58 -2
- package/package.json +7 -3
package/dist/core/provider.js
CHANGED
|
@@ -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
|
|
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);
|
package/dist/hooks/core/index.js
CHANGED
|
@@ -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
|
-
|
|
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({
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
565
|
+
return {
|
|
566
|
+
...baseActions,
|
|
567
|
+
status: 'connecting',
|
|
568
|
+
activeWallet: activeWallet,
|
|
569
|
+
};
|
|
514
570
|
}
|
|
515
571
|
if (status.status === 'error') {
|
|
516
|
-
return {
|
|
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 {
|
|
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, {
|
|
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
|
-
? {
|
|
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({
|
|
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({
|
|
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 {
|
|
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
|
-
|
|
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),
|
package/dist/native/index.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/native/webview.js
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
-
},
|
|
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 '
|
|
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
|
|
@@ -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>;
|
package/dist/types/index.d.ts
CHANGED
package/dist/types/index.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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": {
|