@explorins/pers-sdk-react-native 2.3.7 → 2.3.8
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/LICENSE +21 -0
- package/README.md +1266 -1244
- package/dist/__tests__/PersSDKProvider.test.js +33 -0
- package/dist/hooks/useBusiness.d.ts +2 -2
- package/dist/hooks/useBusiness.d.ts.map +1 -1
- package/dist/hooks/useBusiness.js +2 -2
- package/dist/hooks/useIPFSUrl.d.ts +3 -3
- package/dist/hooks/useIPFSUrl.d.ts.map +1 -1
- package/dist/hooks/useIPFSUrl.js +7 -7
- package/dist/hooks/useWeb3.js +4 -4
- package/dist/index.js +2400 -487
- package/dist/index.js.map +1 -1
- package/dist/providers/PersSDKProvider.d.ts +4 -0
- package/dist/providers/PersSDKProvider.d.ts.map +1 -1
- package/dist/providers/PersSDKProvider.js +14 -0
- package/dist/providers/react-native-http-client.d.ts +2 -0
- package/dist/providers/react-native-http-client.d.ts.map +1 -1
- package/dist/providers/react-native-http-client.js +5 -2
- package/dist/storage/rn-secure-storage.d.ts.map +1 -1
- package/dist/storage/rn-secure-storage.js +21 -8
- package/package.json +3 -3
- package/src/__tests__/PersSDKProvider.test.tsx +484 -420
- package/src/__tests__/setup.ts +40 -40
- package/src/hooks/index.ts +60 -60
- package/src/hooks/useAnalytics.ts +327 -327
- package/src/hooks/useAuth.ts +265 -265
- package/src/hooks/useBookings.ts +191 -191
- package/src/hooks/useBusiness.ts +251 -251
- package/src/hooks/useCampaigns.ts +500 -500
- package/src/hooks/useDonations.ts +66 -66
- package/src/hooks/useEvents.ts +372 -372
- package/src/hooks/useFiles.ts +155 -155
- package/src/hooks/useIPFSUrl.ts +192 -192
- package/src/hooks/usePurchases.ts +65 -65
- package/src/hooks/useRedemptions.ts +467 -467
- package/src/hooks/useTenants.ts +72 -72
- package/src/hooks/useTokenBalances.ts +385 -385
- package/src/hooks/useTokens.ts +313 -313
- package/src/hooks/useTransactionSigner.ts +611 -611
- package/src/hooks/useTransactions.ts +299 -299
- package/src/hooks/useTriggerSources.ts +301 -301
- package/src/hooks/useUserStatus.ts +61 -61
- package/src/hooks/useUsers.ts +167 -167
- package/src/hooks/useWeb3.ts +355 -355
- package/src/index.ts +777 -777
- package/src/polyfills/index.ts +160 -160
- package/src/polyfills/web3-polyfills.ts +97 -97
- package/src/providers/PersSDKProvider.tsx +393 -374
- package/src/providers/react-native-auth-provider.ts +110 -110
- package/src/providers/react-native-http-client.ts +150 -145
- package/src/providers/rn-dpop-provider.ts +190 -190
- package/src/storage/async-storage-token-storage.ts +185 -185
- package/src/storage/rn-secure-storage.ts +182 -170
- package/src/types/react-native-globals.d.ts +45 -45
- package/src/utils/index.ts +8 -8
- package/src/utils/platform-detection.ts +86 -86
- package/dist/index-CzLxqePZ.js +0 -7717
- package/dist/index-CzLxqePZ.js.map +0 -1
|
@@ -1,611 +1,611 @@
|
|
|
1
|
-
import { useCallback, useState, useRef, useEffect } from 'react';
|
|
2
|
-
// import { usePersSDK } from '../providers/PersSDKProvider';
|
|
3
|
-
|
|
4
|
-
// TYPE-ONLY imports from /types entry - NO native module deps, safe at any time
|
|
5
|
-
import type { PersSignerConfig, StatusCallback, StatusUpdateData } from '@explorins/pers-signer/types';
|
|
6
|
-
import type { SigningStatus as SigningStatusType } from '@explorins/pers-signer/types';
|
|
7
|
-
// Runtime const import - also from /types (no native deps)
|
|
8
|
-
import { SigningStatus as SigningStatusConst } from '@explorins/pers-signer/types';
|
|
9
|
-
|
|
10
|
-
// Import actual SDK types from the React Native-specific export
|
|
11
|
-
type PersSignerSDK = import('@explorins/pers-signer/react-native').PersSignerSDK;
|
|
12
|
-
type SubmissionResult = import('@explorins/pers-signer/react-native').SubmissionResult;
|
|
13
|
-
type AuthenticatedUser = import('@explorins/pers-signer/react-native').AuthenticatedUser;
|
|
14
|
-
type TransactionSigningResult = import('@explorins/pers-signer/react-native').TransactionSigningResult;
|
|
15
|
-
|
|
16
|
-
// Re-export the type for external consumers
|
|
17
|
-
export type { SigningStatusType as SigningStatus };
|
|
18
|
-
export type { StatusUpdateData };
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Simple status callback function type for React Native consumers
|
|
22
|
-
* (Simplified from the signer SDK's object-based StatusCallback interface)
|
|
23
|
-
*/
|
|
24
|
-
export type OnStatusUpdateFn = (status: SigningStatusType, message: string, data?: StatusUpdateData) => void;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// Lazy-loaded signer SDK - initialized on first use to avoid native module timing issues
|
|
28
|
-
let _createPersSignerSDK: ((config: PersSignerConfig) => Promise<PersSignerSDK>) | null = null;
|
|
29
|
-
let _signerInitAttempted = false;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Get the SigningStatus const object
|
|
33
|
-
* Now directly imported from /types (no native deps, always available)
|
|
34
|
-
*/
|
|
35
|
-
function getSigningStatus(): typeof SigningStatusType {
|
|
36
|
-
return SigningStatusConst;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Lazily load the PERS Signer SDK.
|
|
41
|
-
*
|
|
42
|
-
* Supports customer pre-initialization via global.__PersSigner for environments
|
|
43
|
-
* where native module timing is unpredictable (e.g., certain React Native setups).
|
|
44
|
-
*
|
|
45
|
-
* @returns The createPersSignerSDK function or null if unavailable
|
|
46
|
-
*/
|
|
47
|
-
function getSignerSDKFactory(): ((config: PersSignerConfig) => Promise<PersSignerSDK>) | null {
|
|
48
|
-
if (_signerInitAttempted) {
|
|
49
|
-
// Already tried - return cached result (could be null if failed)
|
|
50
|
-
return _createPersSignerSDK;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
_signerInitAttempted = true;
|
|
54
|
-
|
|
55
|
-
// Check for customer pre-initialized global (for native module timing issues)
|
|
56
|
-
if (typeof global !== 'undefined' && (global as any).__PersSigner?.createPersSignerSDK) {
|
|
57
|
-
const globalModule = (global as any).__PersSigner;
|
|
58
|
-
_createPersSignerSDK = globalModule.createPersSignerSDK;
|
|
59
|
-
return _createPersSignerSDK;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Standard lazy load
|
|
63
|
-
try {
|
|
64
|
-
const signerModule = require('@explorins/pers-signer/react-native');
|
|
65
|
-
_createPersSignerSDK = signerModule.createPersSignerSDK;
|
|
66
|
-
return _createPersSignerSDK;
|
|
67
|
-
} catch (error: unknown) {
|
|
68
|
-
console.warn('[useTransactionSigner] PERS Signer SDK not available:', (error as Error).message);
|
|
69
|
-
console.warn('[useTransactionSigner] Real blockchain signing will not be available');
|
|
70
|
-
console.warn('[useTransactionSigner] Tip: If timing issues, pre-initialize in index.js: global.__PersSigner = require("@explorins/pers-signer/react-native")');
|
|
71
|
-
_createPersSignerSDK = null; // Mark as unavailable
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Return interface for the useTransactionSigner hook
|
|
79
|
-
*
|
|
80
|
-
* Provides secure transaction signing capabilities for EVM blockchain interactions
|
|
81
|
-
* and error handling for React Native applications.
|
|
82
|
-
*
|
|
83
|
-
* @interface TransactionSignerHook
|
|
84
|
-
* @property {Function} signAndSubmitTransactionWithJWT - Main method to sign and submit transactions
|
|
85
|
-
* @property {Function} signPersTransaction - Sign transaction without submitting (for POS flows)
|
|
86
|
-
* @property {boolean} isSignerInitialized - Whether the signer SDK has been initialized
|
|
87
|
-
* @property {boolean} isSignerAvailable - Whether signing functionality is fully available
|
|
88
|
-
* @property {SigningStatus | null} currentStatus - Current signing status for UI feedback
|
|
89
|
-
* @property {string | null} statusMessage - Human-readable status message
|
|
90
|
-
*/
|
|
91
|
-
export interface TransactionSignerHook {
|
|
92
|
-
signAndSubmitTransactionWithJWT: (jwt: string, onStatusUpdate?: OnStatusUpdateFn) => Promise<SubmissionResult>;
|
|
93
|
-
signPersTransaction: (jwt: string, onStatusUpdate?: OnStatusUpdateFn) => Promise<TransactionSigningResult>;
|
|
94
|
-
isSignerInitialized: boolean;
|
|
95
|
-
isSignerAvailable: boolean;
|
|
96
|
-
currentStatus: SigningStatusType | null;
|
|
97
|
-
statusMessage: string | null;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Constants - TODO: Move to environment config later
|
|
101
|
-
const DEFAULT_ETHERS_PROVIDER = "https://sepolia.infura.io/v3/2781b4b5242343d5b0954c98f287b29e";
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* React Native hook for blockchain transaction signing using PERS Signer SDK
|
|
105
|
-
*
|
|
106
|
-
* This hook provides a complete blockchain transaction signing solution for React Native
|
|
107
|
-
* applications, integrating with the PERS ecosystem for tourism loyalty and reward systems.
|
|
108
|
-
* It automatically handles WebAuthn authentication, transaction preparation, signing, and
|
|
109
|
-
* blockchain submission in a single convenient interface.
|
|
110
|
-
*
|
|
111
|
-
* **Features:**
|
|
112
|
-
* - Automatic WebAuthn provider initialization for React Native
|
|
113
|
-
* - 5-minute authentication caching to reduce repeated logins
|
|
114
|
-
* - Complete transaction lifecycle management (sign + submit)
|
|
115
|
-
* - Comprehensive error handling with detailed error messages
|
|
116
|
-
* - Real-time status monitoring for UI feedback
|
|
117
|
-
* - Automatic retry logic for network failures
|
|
118
|
-
*
|
|
119
|
-
* **Underlying SDK Methods:**
|
|
120
|
-
* The hook uses the PERS Signer SDK which provides 5 core methods:
|
|
121
|
-
* 1. `loginUser(jwtToken)` - Authenticate user with 5-minute caching
|
|
122
|
-
* 2. `signTransaction(signingData, jwtToken)` - Sign transactions with auto-login
|
|
123
|
-
* 3. `submitTransaction(signingResult, jwtToken)` - Submit signed transactions
|
|
124
|
-
* 4. `signPersTransaction(jwtToken)` - Legacy one-liner for backward compatibility
|
|
125
|
-
* 5. `signAndSubmitPersTransaction(jwtToken)` - Complete sign + submit flow (used by this hook)
|
|
126
|
-
*
|
|
127
|
-
* **Security:**
|
|
128
|
-
* - WebAuthn-based secure authentication (no passwords stored)
|
|
129
|
-
* - JWT token validation and expiration checking
|
|
130
|
-
* - Secure transaction signing using device biometrics/PIN
|
|
131
|
-
* - No sensitive data stored locally
|
|
132
|
-
*
|
|
133
|
-
* @returns {TransactionSignerHook} Hook interface with signing methods and status
|
|
134
|
-
*
|
|
135
|
-
* @example
|
|
136
|
-
* **Basic Usage:**
|
|
137
|
-
* ```typescript
|
|
138
|
-
* import { useTransactionSigner } from '@explorins/pers-sdk-react-native';
|
|
139
|
-
*
|
|
140
|
-
* function TransactionScreen() {
|
|
141
|
-
* const {
|
|
142
|
-
* signAndSubmitTransactionWithJWT,
|
|
143
|
-
* isSignerAvailable,
|
|
144
|
-
* isSignerInitialized
|
|
145
|
-
* } = useTransactionSigner();
|
|
146
|
-
*
|
|
147
|
-
* const handleSign = async (jwtFromRedemption: string) => {
|
|
148
|
-
* if (!isSignerAvailable) {
|
|
149
|
-
* console.error('Signer not available');
|
|
150
|
-
* return;
|
|
151
|
-
* }
|
|
152
|
-
*
|
|
153
|
-
* try {
|
|
154
|
-
* const result = await signAndSubmitTransactionWithJWT(jwtFromRedemption);
|
|
155
|
-
* if (result.success) {
|
|
156
|
-
* console.log('Transaction completed:', result.transactionHash);
|
|
157
|
-
* // Handle successful transaction
|
|
158
|
-
* if (result.shouldRedirect && result.redirectUrl) {
|
|
159
|
-
* // Navigate to success page or external URL
|
|
160
|
-
* }
|
|
161
|
-
* }
|
|
162
|
-
* } catch (error) {
|
|
163
|
-
* console.error('Transaction failed:', error.message);
|
|
164
|
-
* // Handle error (show user-friendly message)
|
|
165
|
-
* }
|
|
166
|
-
* };
|
|
167
|
-
*
|
|
168
|
-
* return (
|
|
169
|
-
* <View style={styles.container}>
|
|
170
|
-
* <TouchableOpacity
|
|
171
|
-
* onPress={() => handleSign(redeemJWT)}
|
|
172
|
-
* disabled={!isSignerAvailable}
|
|
173
|
-
* style={[styles.button, !isSignerAvailable && styles.disabled]}
|
|
174
|
-
* >
|
|
175
|
-
* <Text style={styles.buttonText}>
|
|
176
|
-
* {isSignerInitialized ? 'Sign Transaction' : 'Initializing...'}
|
|
177
|
-
* </Text>
|
|
178
|
-
* </TouchableOpacity>
|
|
179
|
-
* </View>
|
|
180
|
-
* );
|
|
181
|
-
* }
|
|
182
|
-
* ```
|
|
183
|
-
*
|
|
184
|
-
* @example
|
|
185
|
-
* **Advanced Usage with Error Handling:**
|
|
186
|
-
* ```typescript
|
|
187
|
-
* function AdvancedTransactionComponent() {
|
|
188
|
-
* const [isLoading, setIsLoading] = useState(false);
|
|
189
|
-
* const [error, setError] = useState<string | null>(null);
|
|
190
|
-
* const { signAndSubmitTransactionWithJWT, isSignerAvailable } = useTransactionSigner();
|
|
191
|
-
*
|
|
192
|
-
* const handleTransaction = async (jwt: string) => {
|
|
193
|
-
* setIsLoading(true);
|
|
194
|
-
* setError(null);
|
|
195
|
-
*
|
|
196
|
-
* try {
|
|
197
|
-
* const result = await signAndSubmitTransactionWithJWT(jwt);
|
|
198
|
-
*
|
|
199
|
-
* if (result.success) {
|
|
200
|
-
* // Success handling
|
|
201
|
-
* Alert.alert(
|
|
202
|
-
* 'Success',
|
|
203
|
-
* `Transaction completed!\nHash: ${result.transactionHash}`
|
|
204
|
-
* );
|
|
205
|
-
* } else {
|
|
206
|
-
* throw new Error(result.error || 'Transaction failed');
|
|
207
|
-
* }
|
|
208
|
-
* } catch (err) {
|
|
209
|
-
* const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
210
|
-
* setError(errorMessage);
|
|
211
|
-
*
|
|
212
|
-
* // Different error handling based on error type
|
|
213
|
-
* if (errorMessage.includes('expired')) {
|
|
214
|
-
* Alert.alert('Session Expired', 'Please log in again');
|
|
215
|
-
* } else if (errorMessage.includes('network')) {
|
|
216
|
-
* Alert.alert('Network Error', 'Please check your connection');
|
|
217
|
-
* } else {
|
|
218
|
-
* Alert.alert('Transaction Failed', errorMessage);
|
|
219
|
-
* }
|
|
220
|
-
* } finally {
|
|
221
|
-
* setIsLoading(false);
|
|
222
|
-
* }
|
|
223
|
-
* };
|
|
224
|
-
*
|
|
225
|
-
* return (
|
|
226
|
-
* <View>
|
|
227
|
-
* {error && (
|
|
228
|
-
* <Text style={styles.errorText}>{error}</Text>
|
|
229
|
-
* )}
|
|
230
|
-
* <TouchableOpacity
|
|
231
|
-
* onPress={() => handleTransaction(jwtToken)}
|
|
232
|
-
* disabled={!isSignerAvailable || isLoading}
|
|
233
|
-
* >
|
|
234
|
-
* <Text>
|
|
235
|
-
* {isLoading ? 'Processing...' : 'Sign & Submit Transaction'}
|
|
236
|
-
* </Text>
|
|
237
|
-
* </TouchableOpacity>
|
|
238
|
-
* </View>
|
|
239
|
-
* );
|
|
240
|
-
* }
|
|
241
|
-
* ```
|
|
242
|
-
*
|
|
243
|
-
* @example
|
|
244
|
-
* **Integration with PERS SDK:**
|
|
245
|
-
* ```typescript
|
|
246
|
-
* import { usePersSDK } from '@explorins/pers-sdk-react-native';
|
|
247
|
-
* import { useTransactionSigner } from '@explorins/pers-sdk-react-native';
|
|
248
|
-
*
|
|
249
|
-
* function RedemptionFlow() {
|
|
250
|
-
* const { user, redeemTokens } = usePersSDK();
|
|
251
|
-
* const { signAndSubmitTransactionWithJWT } = useTransactionSigner();
|
|
252
|
-
*
|
|
253
|
-
* const handleRedemption = async (tokenAmount: number) => {
|
|
254
|
-
* try {
|
|
255
|
-
* // Step 1: Create redemption with PERS SDK
|
|
256
|
-
* const redemption = await redeemTokens({
|
|
257
|
-
* tokenAmount,
|
|
258
|
-
* destinationAddress: user?.walletAddress
|
|
259
|
-
* });
|
|
260
|
-
*
|
|
261
|
-
* // Step 2: Sign and submit transaction
|
|
262
|
-
* const txResult = await signAndSubmitTransactionWithJWT(redemption.jwtToken);
|
|
263
|
-
*
|
|
264
|
-
* if (txResult.success) {
|
|
265
|
-
* console.log('Redemption completed:', txResult.transactionHash);
|
|
266
|
-
* }
|
|
267
|
-
* } catch (error) {
|
|
268
|
-
* console.error('Redemption failed:', error);
|
|
269
|
-
* }
|
|
270
|
-
* };
|
|
271
|
-
* }
|
|
272
|
-
* ```
|
|
273
|
-
*
|
|
274
|
-
* @see {@link SubmissionResult} for transaction result structure
|
|
275
|
-
* @see {@link TransactionSigningResult} for signing result details
|
|
276
|
-
* @see {@link AuthenticatedUser} for user authentication data
|
|
277
|
-
*
|
|
278
|
-
* @since 1.5.0
|
|
279
|
-
*/
|
|
280
|
-
export const useTransactionSigner = (): TransactionSignerHook => {
|
|
281
|
-
// const { isInitialized, isAuthenticated, user } = usePersSDK();
|
|
282
|
-
const [isSignerInitialized, setIsSignerInitialized] = useState(false);
|
|
283
|
-
const [currentStatus, setCurrentStatus] = useState<SigningStatusType | null>(null);
|
|
284
|
-
const [statusMessage, setStatusMessage] = useState<string | null>(null);
|
|
285
|
-
const signerSDKRef = useRef<PersSignerSDK | null>(null);
|
|
286
|
-
|
|
287
|
-
// Auto-initialize signer SDK when PERS SDK is ready
|
|
288
|
-
useEffect(() => {
|
|
289
|
-
const initializeSignerSDK = async () => {
|
|
290
|
-
const createPersSignerSDK = getSignerSDKFactory();
|
|
291
|
-
if (!createPersSignerSDK) {
|
|
292
|
-
console.warn('[useTransactionSigner] Signer SDK not available');
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
try {
|
|
297
|
-
// React Native createPersSignerSDK automatically includes WebAuthn provider
|
|
298
|
-
const signerSDK = await createPersSignerSDK({
|
|
299
|
-
ethersProviderUrl: DEFAULT_ETHERS_PROVIDER,
|
|
300
|
-
relyingPartyName: 'PERS React Native App'
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
signerSDKRef.current = signerSDK;
|
|
304
|
-
setIsSignerInitialized(true);
|
|
305
|
-
|
|
306
|
-
} catch (error) {
|
|
307
|
-
console.error('[useTransactionSigner] Failed to initialize signer SDK:', error);
|
|
308
|
-
}
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
if (!isSignerInitialized) {
|
|
312
|
-
initializeSignerSDK();
|
|
313
|
-
}
|
|
314
|
-
}, [isSignerInitialized]);
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Sign and submit a blockchain transaction using JWT token
|
|
318
|
-
*
|
|
319
|
-
* This is the main method for executing blockchain transactions in React Native.
|
|
320
|
-
* It handles the complete transaction lifecycle: authentication, signing, and
|
|
321
|
-
* blockchain submission in a single call.
|
|
322
|
-
*
|
|
323
|
-
* **Process Flow:**
|
|
324
|
-
* 1. Validates the JWT token and extracts transaction data
|
|
325
|
-
* 2. Authenticates the user using WebAuthn (cached for 5 minutes)
|
|
326
|
-
* 3. Fetches transaction data from PERS backend
|
|
327
|
-
* 4. Signs the transaction using device security (biometrics/PIN)
|
|
328
|
-
* 5. Submits the signed transaction to the blockchain
|
|
329
|
-
* 6. Returns detailed result with transaction hash and status
|
|
330
|
-
*
|
|
331
|
-
* **Security Features:**
|
|
332
|
-
* - JWT token validation and expiration checking
|
|
333
|
-
* - WebAuthn authentication with device biometrics
|
|
334
|
-
* - Secure transaction signing without exposing private keys
|
|
335
|
-
* - Automatic session management with secure caching
|
|
336
|
-
*
|
|
337
|
-
* @param {string} jwt - JWT token containing transaction ID and user information
|
|
338
|
-
* Must include: `transactionId`, `identifierEmail`, `tenantId`
|
|
339
|
-
* @param {StatusCallback} [onStatusUpdate] - Optional callback for real-time status updates
|
|
340
|
-
* @returns {Promise<SubmissionResult>} Complete transaction result
|
|
341
|
-
*
|
|
342
|
-
* @throws {Error} 'Transaction signer not initialized' - Hook not ready
|
|
343
|
-
* @throws {Error} 'PERS Signer SDK not available' - SDK installation issue
|
|
344
|
-
* @throws {Error} 'Invalid or expired JWT token' - Token validation failed
|
|
345
|
-
* @throws {Error} 'Authentication failed' - WebAuthn authentication failed
|
|
346
|
-
* @throws {Error} 'Transaction signing failed' - Signing process failed
|
|
347
|
-
* @throws {Error} 'Transaction submission failed' - Blockchain submission failed
|
|
348
|
-
*
|
|
349
|
-
* @example
|
|
350
|
-
* **Basic Transaction:**
|
|
351
|
-
* ```typescript
|
|
352
|
-
* const { signAndSubmitTransactionWithJWT } = useTransactionSigner();
|
|
353
|
-
*
|
|
354
|
-
* const result = await signAndSubmitTransactionWithJWT(jwtToken);
|
|
355
|
-
* if (result.success) {
|
|
356
|
-
* console.log('Transaction Hash:', result.transactionHash);
|
|
357
|
-
* console.log('Blockchain explorer: [Chain-specific explorer URL]');
|
|
358
|
-
* }
|
|
359
|
-
* ```
|
|
360
|
-
*
|
|
361
|
-
* @example
|
|
362
|
-
* **With Status Updates:**
|
|
363
|
-
* ```typescript
|
|
364
|
-
* const result = await signAndSubmitTransactionWithJWT(jwtToken, (status, message) => {
|
|
365
|
-
* console.log(`Status: ${status} - ${message}`);
|
|
366
|
-
* setStatusText(message);
|
|
367
|
-
* });
|
|
368
|
-
* ```
|
|
369
|
-
*
|
|
370
|
-
* @example
|
|
371
|
-
* **With Error Handling:**
|
|
372
|
-
* ```typescript
|
|
373
|
-
* try {
|
|
374
|
-
* const result = await signAndSubmitTransactionWithJWT(redeemJWT);
|
|
375
|
-
*
|
|
376
|
-
* if (result.success) {
|
|
377
|
-
* // Transaction successful
|
|
378
|
-
* setTransactionHash(result.transactionHash);
|
|
379
|
-
*
|
|
380
|
-
* if (result.shouldRedirect && result.redirectUrl) {
|
|
381
|
-
* // Handle post-transaction redirect
|
|
382
|
-
* Linking.openURL(result.redirectUrl);
|
|
383
|
-
* }
|
|
384
|
-
* } else {
|
|
385
|
-
* // Transaction failed but didn't throw
|
|
386
|
-
* setError(result.error || 'Transaction failed');
|
|
387
|
-
* }
|
|
388
|
-
* } catch (error) {
|
|
389
|
-
* // Handle different error types
|
|
390
|
-
* if (error.message.includes('expired')) {
|
|
391
|
-
* // Token expired - redirect to login
|
|
392
|
-
* navigation.navigate('Login');
|
|
393
|
-
* } else if (error.message.includes('network')) {
|
|
394
|
-
* // Network error - suggest retry
|
|
395
|
-
* setError('Network error. Please try again.');
|
|
396
|
-
* } else {
|
|
397
|
-
* // Other errors
|
|
398
|
-
* setError(error.message);
|
|
399
|
-
* }
|
|
400
|
-
* }
|
|
401
|
-
* ```
|
|
402
|
-
*
|
|
403
|
-
* @see {@link SubmissionResult} for detailed result structure
|
|
404
|
-
*/
|
|
405
|
-
const signAndSubmitTransactionWithJWT = useCallback(async (jwt: string, onStatusUpdate?: OnStatusUpdateFn): Promise<SubmissionResult> => {
|
|
406
|
-
if (!isSignerInitialized || !signerSDKRef.current) {
|
|
407
|
-
throw new Error('Transaction signer not initialized');
|
|
408
|
-
}
|
|
409
|
-
if (!getSignerSDKFactory()) {
|
|
410
|
-
throw new Error('PERS Signer SDK not available. Blockchain signing is not supported.');
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Create status callback wrapper that updates both hook state and calls user callback
|
|
414
|
-
const statusCallback: StatusCallback = {
|
|
415
|
-
onStatusUpdate: (status: SigningStatusType, message: string, data?: StatusUpdateData) => {
|
|
416
|
-
setCurrentStatus(status);
|
|
417
|
-
setStatusMessage(message);
|
|
418
|
-
onStatusUpdate?.(status, message, data);
|
|
419
|
-
}
|
|
420
|
-
};
|
|
421
|
-
|
|
422
|
-
try {
|
|
423
|
-
// Use the actual SDK method that handles the complete sign + submit flow
|
|
424
|
-
const submissionResult = await signerSDKRef.current.signAndSubmitPersTransaction(jwt, statusCallback);
|
|
425
|
-
|
|
426
|
-
return submissionResult;
|
|
427
|
-
|
|
428
|
-
} catch (error) {
|
|
429
|
-
console.error('[useTransactionSigner] JWT transaction signing failed:', error);
|
|
430
|
-
const SigningStatusEnum = getSigningStatus();
|
|
431
|
-
if (SigningStatusEnum) setCurrentStatus(SigningStatusEnum.ERROR);
|
|
432
|
-
setStatusMessage(error instanceof Error ? error.message : 'Transaction failed');
|
|
433
|
-
throw error; // Re-throw to maintain error handling upstream
|
|
434
|
-
}
|
|
435
|
-
}, [isSignerInitialized]);
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* Sign a PERS transaction without submitting (for POS flows with QR codes)
|
|
439
|
-
*
|
|
440
|
-
* This method signs the transaction but does NOT submit it to the blockchain.
|
|
441
|
-
* Instead, it returns the signature which can be displayed as a QR code for
|
|
442
|
-
* the business to scan and submit.
|
|
443
|
-
*
|
|
444
|
-
* **Use Case:** Point-of-Sale (POS) flows where:
|
|
445
|
-
* 1. User signs the transaction
|
|
446
|
-
* 2. App generates QR code with signature
|
|
447
|
-
* 3. Business scans QR code
|
|
448
|
-
* 4. Business submits transaction to blockchain
|
|
449
|
-
*
|
|
450
|
-
* @param {string} jwt - JWT token with transaction and user data
|
|
451
|
-
* @param {StatusCallback} [onStatusUpdate] - Optional callback for status updates
|
|
452
|
-
* @returns {Promise<TransactionSigningResult>} Signing result with signature for QR code
|
|
453
|
-
*
|
|
454
|
-
* @example
|
|
455
|
-
* ```typescript
|
|
456
|
-
* const { signPersTransaction } = useTransactionSigner();
|
|
457
|
-
*
|
|
458
|
-
* const result = await signPersTransaction(jwtToken);
|
|
459
|
-
* if (result.success) {
|
|
460
|
-
* const qrData = {
|
|
461
|
-
* transactionId: result.transactionId,
|
|
462
|
-
* signature: result.signature,
|
|
463
|
-
* transactionFormat: result.signingData.format,
|
|
464
|
-
* txType: 'PENDING_SUBMISSION'
|
|
465
|
-
* };
|
|
466
|
-
* // Display QR code with qrData
|
|
467
|
-
* }
|
|
468
|
-
* ```
|
|
469
|
-
*/
|
|
470
|
-
const signPersTransaction = useCallback(async (jwt: string, onStatusUpdate?: OnStatusUpdateFn): Promise<TransactionSigningResult> => {
|
|
471
|
-
if (!isSignerInitialized || !signerSDKRef.current) {
|
|
472
|
-
throw new Error('Signer SDK not initialized');
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// Create status callback wrapper
|
|
476
|
-
const statusCallback: StatusCallback = {
|
|
477
|
-
onStatusUpdate: (status: SigningStatusType, message: string, data?: StatusUpdateData) => {
|
|
478
|
-
setCurrentStatus(status);
|
|
479
|
-
setStatusMessage(message);
|
|
480
|
-
onStatusUpdate?.(status, message, data);
|
|
481
|
-
}
|
|
482
|
-
};
|
|
483
|
-
|
|
484
|
-
try {
|
|
485
|
-
// Use the SDK's sign-only method
|
|
486
|
-
const signingResult = await signerSDKRef.current.signPersTransaction(jwt, statusCallback);
|
|
487
|
-
return signingResult;
|
|
488
|
-
} catch (error) {
|
|
489
|
-
console.error('[useTransactionSigner] Sign-only transaction failed:', error);
|
|
490
|
-
const SigningStatusEnum = getSigningStatus();
|
|
491
|
-
if (SigningStatusEnum) setCurrentStatus(SigningStatusEnum.ERROR);
|
|
492
|
-
setStatusMessage(error instanceof Error ? error.message : 'Signing failed');
|
|
493
|
-
throw error;
|
|
494
|
-
}
|
|
495
|
-
}, [isSignerInitialized]);
|
|
496
|
-
|
|
497
|
-
return {
|
|
498
|
-
/**
|
|
499
|
-
* Sign and submit a blockchain transaction using JWT token
|
|
500
|
-
*
|
|
501
|
-
* Main method for executing blockchain transactions. Handles complete flow
|
|
502
|
-
* from authentication to blockchain submission in a single call.
|
|
503
|
-
*
|
|
504
|
-
* @param {string} jwt - JWT token with transaction and user data
|
|
505
|
-
* @param {StatusCallback} [onStatusUpdate] - Optional callback for real-time status updates
|
|
506
|
-
* @returns {Promise<SubmissionResult>} Transaction result with hash and status
|
|
507
|
-
*/
|
|
508
|
-
signAndSubmitTransactionWithJWT,
|
|
509
|
-
|
|
510
|
-
/**
|
|
511
|
-
* Sign a transaction without submitting (for POS flows)
|
|
512
|
-
*
|
|
513
|
-
* Signs the transaction and returns the signature without submitting to blockchain.
|
|
514
|
-
* Use this for Point-of-Sale flows where the business scans a QR code and submits.
|
|
515
|
-
*
|
|
516
|
-
* @param {string} jwt - JWT token with transaction and user data
|
|
517
|
-
* @param {StatusCallback} [onStatusUpdate] - Optional callback for status updates
|
|
518
|
-
* @returns {Promise<TransactionSigningResult>} Signing result with signature for QR code
|
|
519
|
-
*/
|
|
520
|
-
signPersTransaction,
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
* Whether the transaction signer SDK has been successfully initialized
|
|
524
|
-
*
|
|
525
|
-
* Use this to show loading states while the signer is being set up.
|
|
526
|
-
* When `false`, the signer is still initializing and transactions cannot be processed.
|
|
527
|
-
*
|
|
528
|
-
* @example
|
|
529
|
-
* ```typescript
|
|
530
|
-
* const { isSignerInitialized } = useTransactionSigner();
|
|
531
|
-
*
|
|
532
|
-
* if (!isSignerInitialized) {
|
|
533
|
-
* return <LoadingSpinner text="Initializing signer..." />;
|
|
534
|
-
* }
|
|
535
|
-
* ```
|
|
536
|
-
*/
|
|
537
|
-
isSignerInitialized,
|
|
538
|
-
|
|
539
|
-
/**
|
|
540
|
-
* Whether blockchain transaction signing is fully available
|
|
541
|
-
*
|
|
542
|
-
* This combines multiple checks: SDK availability, initialization status,
|
|
543
|
-
* and configuration validity. Use this to enable/disable transaction buttons.
|
|
544
|
-
*
|
|
545
|
-
* Returns `true` only when:
|
|
546
|
-
* - PERS Signer SDK is installed and available
|
|
547
|
-
* - Signer has been successfully initialized
|
|
548
|
-
* - All required dependencies are loaded
|
|
549
|
-
*
|
|
550
|
-
* @example
|
|
551
|
-
* ```typescript
|
|
552
|
-
* const { isSignerAvailable } = useTransactionSigner();
|
|
553
|
-
*
|
|
554
|
-
* return (
|
|
555
|
-
* <TouchableOpacity
|
|
556
|
-
* disabled={!isSignerAvailable}
|
|
557
|
-
* style={[styles.button, !isSignerAvailable && styles.disabled]}
|
|
558
|
-
* >
|
|
559
|
-
* <Text>{isSignerAvailable ? 'Sign Transaction' : 'Signer Unavailable'}</Text>
|
|
560
|
-
* </TouchableOpacity>
|
|
561
|
-
* );
|
|
562
|
-
* ```
|
|
563
|
-
*/
|
|
564
|
-
isSignerAvailable: isSignerInitialized && !!getSignerSDKFactory(),
|
|
565
|
-
|
|
566
|
-
/**
|
|
567
|
-
* Current signing status for UI feedback
|
|
568
|
-
*
|
|
569
|
-
* Tracks the current state of the signing process. Use this to show
|
|
570
|
-
* progress indicators or status messages during transaction signing.
|
|
571
|
-
*
|
|
572
|
-
* Possible values: 'initializing', 'authenticating', 'preparing',
|
|
573
|
-
* 'signing', 'submitting', 'completed', 'error', 'expired'
|
|
574
|
-
*
|
|
575
|
-
* @example
|
|
576
|
-
* ```typescript
|
|
577
|
-
* const { currentStatus, statusMessage } = useTransactionSigner();
|
|
578
|
-
*
|
|
579
|
-
* return (
|
|
580
|
-
* <View>
|
|
581
|
-
* {currentStatus && (
|
|
582
|
-
* <Text>Status: {statusMessage}</Text>
|
|
583
|
-
* )}
|
|
584
|
-
* </View>
|
|
585
|
-
* );
|
|
586
|
-
* ```
|
|
587
|
-
*/
|
|
588
|
-
currentStatus,
|
|
589
|
-
|
|
590
|
-
/**
|
|
591
|
-
* Human-readable status message
|
|
592
|
-
*
|
|
593
|
-
* Provides a user-friendly description of the current signing status.
|
|
594
|
-
* Updates in real-time as the transaction progresses.
|
|
595
|
-
*/
|
|
596
|
-
statusMessage,
|
|
597
|
-
};
|
|
598
|
-
};
|
|
599
|
-
|
|
600
|
-
/**
|
|
601
|
-
* Type exports for external usage
|
|
602
|
-
*
|
|
603
|
-
* These types are re-exported from the PERS Signer SDK for convenience
|
|
604
|
-
* when working with transaction signing in React Native applications.
|
|
605
|
-
*
|
|
606
|
-
* @see {@link SubmissionResult} - Complete transaction submission result
|
|
607
|
-
* @see {@link AuthenticatedUser} - User authentication data structure
|
|
608
|
-
* @see {@link TransactionSigningResult} - Transaction signing result details
|
|
609
|
-
* @see {@link TransactionSignerHook} - Hook return interface
|
|
610
|
-
*/
|
|
611
|
-
export type { SubmissionResult, AuthenticatedUser, TransactionSigningResult };
|
|
1
|
+
import { useCallback, useState, useRef, useEffect } from 'react';
|
|
2
|
+
// import { usePersSDK } from '../providers/PersSDKProvider';
|
|
3
|
+
|
|
4
|
+
// TYPE-ONLY imports from /types entry - NO native module deps, safe at any time
|
|
5
|
+
import type { PersSignerConfig, StatusCallback, StatusUpdateData } from '@explorins/pers-signer/types';
|
|
6
|
+
import type { SigningStatus as SigningStatusType } from '@explorins/pers-signer/types';
|
|
7
|
+
// Runtime const import - also from /types (no native deps)
|
|
8
|
+
import { SigningStatus as SigningStatusConst } from '@explorins/pers-signer/types';
|
|
9
|
+
|
|
10
|
+
// Import actual SDK types from the React Native-specific export
|
|
11
|
+
type PersSignerSDK = import('@explorins/pers-signer/react-native').PersSignerSDK;
|
|
12
|
+
type SubmissionResult = import('@explorins/pers-signer/react-native').SubmissionResult;
|
|
13
|
+
type AuthenticatedUser = import('@explorins/pers-signer/react-native').AuthenticatedUser;
|
|
14
|
+
type TransactionSigningResult = import('@explorins/pers-signer/react-native').TransactionSigningResult;
|
|
15
|
+
|
|
16
|
+
// Re-export the type for external consumers
|
|
17
|
+
export type { SigningStatusType as SigningStatus };
|
|
18
|
+
export type { StatusUpdateData };
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Simple status callback function type for React Native consumers
|
|
22
|
+
* (Simplified from the signer SDK's object-based StatusCallback interface)
|
|
23
|
+
*/
|
|
24
|
+
export type OnStatusUpdateFn = (status: SigningStatusType, message: string, data?: StatusUpdateData) => void;
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
// Lazy-loaded signer SDK - initialized on first use to avoid native module timing issues
|
|
28
|
+
let _createPersSignerSDK: ((config: PersSignerConfig) => Promise<PersSignerSDK>) | null = null;
|
|
29
|
+
let _signerInitAttempted = false;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get the SigningStatus const object
|
|
33
|
+
* Now directly imported from /types (no native deps, always available)
|
|
34
|
+
*/
|
|
35
|
+
function getSigningStatus(): typeof SigningStatusType {
|
|
36
|
+
return SigningStatusConst;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Lazily load the PERS Signer SDK.
|
|
41
|
+
*
|
|
42
|
+
* Supports customer pre-initialization via global.__PersSigner for environments
|
|
43
|
+
* where native module timing is unpredictable (e.g., certain React Native setups).
|
|
44
|
+
*
|
|
45
|
+
* @returns The createPersSignerSDK function or null if unavailable
|
|
46
|
+
*/
|
|
47
|
+
function getSignerSDKFactory(): ((config: PersSignerConfig) => Promise<PersSignerSDK>) | null {
|
|
48
|
+
if (_signerInitAttempted) {
|
|
49
|
+
// Already tried - return cached result (could be null if failed)
|
|
50
|
+
return _createPersSignerSDK;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_signerInitAttempted = true;
|
|
54
|
+
|
|
55
|
+
// Check for customer pre-initialized global (for native module timing issues)
|
|
56
|
+
if (typeof global !== 'undefined' && (global as any).__PersSigner?.createPersSignerSDK) {
|
|
57
|
+
const globalModule = (global as any).__PersSigner;
|
|
58
|
+
_createPersSignerSDK = globalModule.createPersSignerSDK;
|
|
59
|
+
return _createPersSignerSDK;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Standard lazy load
|
|
63
|
+
try {
|
|
64
|
+
const signerModule = require('@explorins/pers-signer/react-native');
|
|
65
|
+
_createPersSignerSDK = signerModule.createPersSignerSDK;
|
|
66
|
+
return _createPersSignerSDK;
|
|
67
|
+
} catch (error: unknown) {
|
|
68
|
+
console.warn('[useTransactionSigner] PERS Signer SDK not available:', (error as Error).message);
|
|
69
|
+
console.warn('[useTransactionSigner] Real blockchain signing will not be available');
|
|
70
|
+
console.warn('[useTransactionSigner] Tip: If timing issues, pre-initialize in index.js: global.__PersSigner = require("@explorins/pers-signer/react-native")');
|
|
71
|
+
_createPersSignerSDK = null; // Mark as unavailable
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Return interface for the useTransactionSigner hook
|
|
79
|
+
*
|
|
80
|
+
* Provides secure transaction signing capabilities for EVM blockchain interactions
|
|
81
|
+
* and error handling for React Native applications.
|
|
82
|
+
*
|
|
83
|
+
* @interface TransactionSignerHook
|
|
84
|
+
* @property {Function} signAndSubmitTransactionWithJWT - Main method to sign and submit transactions
|
|
85
|
+
* @property {Function} signPersTransaction - Sign transaction without submitting (for POS flows)
|
|
86
|
+
* @property {boolean} isSignerInitialized - Whether the signer SDK has been initialized
|
|
87
|
+
* @property {boolean} isSignerAvailable - Whether signing functionality is fully available
|
|
88
|
+
* @property {SigningStatus | null} currentStatus - Current signing status for UI feedback
|
|
89
|
+
* @property {string | null} statusMessage - Human-readable status message
|
|
90
|
+
*/
|
|
91
|
+
export interface TransactionSignerHook {
|
|
92
|
+
signAndSubmitTransactionWithJWT: (jwt: string, onStatusUpdate?: OnStatusUpdateFn) => Promise<SubmissionResult>;
|
|
93
|
+
signPersTransaction: (jwt: string, onStatusUpdate?: OnStatusUpdateFn) => Promise<TransactionSigningResult>;
|
|
94
|
+
isSignerInitialized: boolean;
|
|
95
|
+
isSignerAvailable: boolean;
|
|
96
|
+
currentStatus: SigningStatusType | null;
|
|
97
|
+
statusMessage: string | null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Constants - TODO: Move to environment config later
|
|
101
|
+
const DEFAULT_ETHERS_PROVIDER = "https://sepolia.infura.io/v3/2781b4b5242343d5b0954c98f287b29e";
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* React Native hook for blockchain transaction signing using PERS Signer SDK
|
|
105
|
+
*
|
|
106
|
+
* This hook provides a complete blockchain transaction signing solution for React Native
|
|
107
|
+
* applications, integrating with the PERS ecosystem for tourism loyalty and reward systems.
|
|
108
|
+
* It automatically handles WebAuthn authentication, transaction preparation, signing, and
|
|
109
|
+
* blockchain submission in a single convenient interface.
|
|
110
|
+
*
|
|
111
|
+
* **Features:**
|
|
112
|
+
* - Automatic WebAuthn provider initialization for React Native
|
|
113
|
+
* - 5-minute authentication caching to reduce repeated logins
|
|
114
|
+
* - Complete transaction lifecycle management (sign + submit)
|
|
115
|
+
* - Comprehensive error handling with detailed error messages
|
|
116
|
+
* - Real-time status monitoring for UI feedback
|
|
117
|
+
* - Automatic retry logic for network failures
|
|
118
|
+
*
|
|
119
|
+
* **Underlying SDK Methods:**
|
|
120
|
+
* The hook uses the PERS Signer SDK which provides 5 core methods:
|
|
121
|
+
* 1. `loginUser(jwtToken)` - Authenticate user with 5-minute caching
|
|
122
|
+
* 2. `signTransaction(signingData, jwtToken)` - Sign transactions with auto-login
|
|
123
|
+
* 3. `submitTransaction(signingResult, jwtToken)` - Submit signed transactions
|
|
124
|
+
* 4. `signPersTransaction(jwtToken)` - Legacy one-liner for backward compatibility
|
|
125
|
+
* 5. `signAndSubmitPersTransaction(jwtToken)` - Complete sign + submit flow (used by this hook)
|
|
126
|
+
*
|
|
127
|
+
* **Security:**
|
|
128
|
+
* - WebAuthn-based secure authentication (no passwords stored)
|
|
129
|
+
* - JWT token validation and expiration checking
|
|
130
|
+
* - Secure transaction signing using device biometrics/PIN
|
|
131
|
+
* - No sensitive data stored locally
|
|
132
|
+
*
|
|
133
|
+
* @returns {TransactionSignerHook} Hook interface with signing methods and status
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* **Basic Usage:**
|
|
137
|
+
* ```typescript
|
|
138
|
+
* import { useTransactionSigner } from '@explorins/pers-sdk-react-native';
|
|
139
|
+
*
|
|
140
|
+
* function TransactionScreen() {
|
|
141
|
+
* const {
|
|
142
|
+
* signAndSubmitTransactionWithJWT,
|
|
143
|
+
* isSignerAvailable,
|
|
144
|
+
* isSignerInitialized
|
|
145
|
+
* } = useTransactionSigner();
|
|
146
|
+
*
|
|
147
|
+
* const handleSign = async (jwtFromRedemption: string) => {
|
|
148
|
+
* if (!isSignerAvailable) {
|
|
149
|
+
* console.error('Signer not available');
|
|
150
|
+
* return;
|
|
151
|
+
* }
|
|
152
|
+
*
|
|
153
|
+
* try {
|
|
154
|
+
* const result = await signAndSubmitTransactionWithJWT(jwtFromRedemption);
|
|
155
|
+
* if (result.success) {
|
|
156
|
+
* console.log('Transaction completed:', result.transactionHash);
|
|
157
|
+
* // Handle successful transaction
|
|
158
|
+
* if (result.shouldRedirect && result.redirectUrl) {
|
|
159
|
+
* // Navigate to success page or external URL
|
|
160
|
+
* }
|
|
161
|
+
* }
|
|
162
|
+
* } catch (error) {
|
|
163
|
+
* console.error('Transaction failed:', error.message);
|
|
164
|
+
* // Handle error (show user-friendly message)
|
|
165
|
+
* }
|
|
166
|
+
* };
|
|
167
|
+
*
|
|
168
|
+
* return (
|
|
169
|
+
* <View style={styles.container}>
|
|
170
|
+
* <TouchableOpacity
|
|
171
|
+
* onPress={() => handleSign(redeemJWT)}
|
|
172
|
+
* disabled={!isSignerAvailable}
|
|
173
|
+
* style={[styles.button, !isSignerAvailable && styles.disabled]}
|
|
174
|
+
* >
|
|
175
|
+
* <Text style={styles.buttonText}>
|
|
176
|
+
* {isSignerInitialized ? 'Sign Transaction' : 'Initializing...'}
|
|
177
|
+
* </Text>
|
|
178
|
+
* </TouchableOpacity>
|
|
179
|
+
* </View>
|
|
180
|
+
* );
|
|
181
|
+
* }
|
|
182
|
+
* ```
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* **Advanced Usage with Error Handling:**
|
|
186
|
+
* ```typescript
|
|
187
|
+
* function AdvancedTransactionComponent() {
|
|
188
|
+
* const [isLoading, setIsLoading] = useState(false);
|
|
189
|
+
* const [error, setError] = useState<string | null>(null);
|
|
190
|
+
* const { signAndSubmitTransactionWithJWT, isSignerAvailable } = useTransactionSigner();
|
|
191
|
+
*
|
|
192
|
+
* const handleTransaction = async (jwt: string) => {
|
|
193
|
+
* setIsLoading(true);
|
|
194
|
+
* setError(null);
|
|
195
|
+
*
|
|
196
|
+
* try {
|
|
197
|
+
* const result = await signAndSubmitTransactionWithJWT(jwt);
|
|
198
|
+
*
|
|
199
|
+
* if (result.success) {
|
|
200
|
+
* // Success handling
|
|
201
|
+
* Alert.alert(
|
|
202
|
+
* 'Success',
|
|
203
|
+
* `Transaction completed!\nHash: ${result.transactionHash}`
|
|
204
|
+
* );
|
|
205
|
+
* } else {
|
|
206
|
+
* throw new Error(result.error || 'Transaction failed');
|
|
207
|
+
* }
|
|
208
|
+
* } catch (err) {
|
|
209
|
+
* const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
210
|
+
* setError(errorMessage);
|
|
211
|
+
*
|
|
212
|
+
* // Different error handling based on error type
|
|
213
|
+
* if (errorMessage.includes('expired')) {
|
|
214
|
+
* Alert.alert('Session Expired', 'Please log in again');
|
|
215
|
+
* } else if (errorMessage.includes('network')) {
|
|
216
|
+
* Alert.alert('Network Error', 'Please check your connection');
|
|
217
|
+
* } else {
|
|
218
|
+
* Alert.alert('Transaction Failed', errorMessage);
|
|
219
|
+
* }
|
|
220
|
+
* } finally {
|
|
221
|
+
* setIsLoading(false);
|
|
222
|
+
* }
|
|
223
|
+
* };
|
|
224
|
+
*
|
|
225
|
+
* return (
|
|
226
|
+
* <View>
|
|
227
|
+
* {error && (
|
|
228
|
+
* <Text style={styles.errorText}>{error}</Text>
|
|
229
|
+
* )}
|
|
230
|
+
* <TouchableOpacity
|
|
231
|
+
* onPress={() => handleTransaction(jwtToken)}
|
|
232
|
+
* disabled={!isSignerAvailable || isLoading}
|
|
233
|
+
* >
|
|
234
|
+
* <Text>
|
|
235
|
+
* {isLoading ? 'Processing...' : 'Sign & Submit Transaction'}
|
|
236
|
+
* </Text>
|
|
237
|
+
* </TouchableOpacity>
|
|
238
|
+
* </View>
|
|
239
|
+
* );
|
|
240
|
+
* }
|
|
241
|
+
* ```
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* **Integration with PERS SDK:**
|
|
245
|
+
* ```typescript
|
|
246
|
+
* import { usePersSDK } from '@explorins/pers-sdk-react-native';
|
|
247
|
+
* import { useTransactionSigner } from '@explorins/pers-sdk-react-native';
|
|
248
|
+
*
|
|
249
|
+
* function RedemptionFlow() {
|
|
250
|
+
* const { user, redeemTokens } = usePersSDK();
|
|
251
|
+
* const { signAndSubmitTransactionWithJWT } = useTransactionSigner();
|
|
252
|
+
*
|
|
253
|
+
* const handleRedemption = async (tokenAmount: number) => {
|
|
254
|
+
* try {
|
|
255
|
+
* // Step 1: Create redemption with PERS SDK
|
|
256
|
+
* const redemption = await redeemTokens({
|
|
257
|
+
* tokenAmount,
|
|
258
|
+
* destinationAddress: user?.walletAddress
|
|
259
|
+
* });
|
|
260
|
+
*
|
|
261
|
+
* // Step 2: Sign and submit transaction
|
|
262
|
+
* const txResult = await signAndSubmitTransactionWithJWT(redemption.jwtToken);
|
|
263
|
+
*
|
|
264
|
+
* if (txResult.success) {
|
|
265
|
+
* console.log('Redemption completed:', txResult.transactionHash);
|
|
266
|
+
* }
|
|
267
|
+
* } catch (error) {
|
|
268
|
+
* console.error('Redemption failed:', error);
|
|
269
|
+
* }
|
|
270
|
+
* };
|
|
271
|
+
* }
|
|
272
|
+
* ```
|
|
273
|
+
*
|
|
274
|
+
* @see {@link SubmissionResult} for transaction result structure
|
|
275
|
+
* @see {@link TransactionSigningResult} for signing result details
|
|
276
|
+
* @see {@link AuthenticatedUser} for user authentication data
|
|
277
|
+
*
|
|
278
|
+
* @since 1.5.0
|
|
279
|
+
*/
|
|
280
|
+
export const useTransactionSigner = (): TransactionSignerHook => {
|
|
281
|
+
// const { isInitialized, isAuthenticated, user } = usePersSDK();
|
|
282
|
+
const [isSignerInitialized, setIsSignerInitialized] = useState(false);
|
|
283
|
+
const [currentStatus, setCurrentStatus] = useState<SigningStatusType | null>(null);
|
|
284
|
+
const [statusMessage, setStatusMessage] = useState<string | null>(null);
|
|
285
|
+
const signerSDKRef = useRef<PersSignerSDK | null>(null);
|
|
286
|
+
|
|
287
|
+
// Auto-initialize signer SDK when PERS SDK is ready
|
|
288
|
+
useEffect(() => {
|
|
289
|
+
const initializeSignerSDK = async () => {
|
|
290
|
+
const createPersSignerSDK = getSignerSDKFactory();
|
|
291
|
+
if (!createPersSignerSDK) {
|
|
292
|
+
console.warn('[useTransactionSigner] Signer SDK not available');
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
// React Native createPersSignerSDK automatically includes WebAuthn provider
|
|
298
|
+
const signerSDK = await createPersSignerSDK({
|
|
299
|
+
ethersProviderUrl: DEFAULT_ETHERS_PROVIDER,
|
|
300
|
+
relyingPartyName: 'PERS React Native App'
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
signerSDKRef.current = signerSDK;
|
|
304
|
+
setIsSignerInitialized(true);
|
|
305
|
+
|
|
306
|
+
} catch (error) {
|
|
307
|
+
console.error('[useTransactionSigner] Failed to initialize signer SDK:', error);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
if (!isSignerInitialized) {
|
|
312
|
+
initializeSignerSDK();
|
|
313
|
+
}
|
|
314
|
+
}, [isSignerInitialized]);
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Sign and submit a blockchain transaction using JWT token
|
|
318
|
+
*
|
|
319
|
+
* This is the main method for executing blockchain transactions in React Native.
|
|
320
|
+
* It handles the complete transaction lifecycle: authentication, signing, and
|
|
321
|
+
* blockchain submission in a single call.
|
|
322
|
+
*
|
|
323
|
+
* **Process Flow:**
|
|
324
|
+
* 1. Validates the JWT token and extracts transaction data
|
|
325
|
+
* 2. Authenticates the user using WebAuthn (cached for 5 minutes)
|
|
326
|
+
* 3. Fetches transaction data from PERS backend
|
|
327
|
+
* 4. Signs the transaction using device security (biometrics/PIN)
|
|
328
|
+
* 5. Submits the signed transaction to the blockchain
|
|
329
|
+
* 6. Returns detailed result with transaction hash and status
|
|
330
|
+
*
|
|
331
|
+
* **Security Features:**
|
|
332
|
+
* - JWT token validation and expiration checking
|
|
333
|
+
* - WebAuthn authentication with device biometrics
|
|
334
|
+
* - Secure transaction signing without exposing private keys
|
|
335
|
+
* - Automatic session management with secure caching
|
|
336
|
+
*
|
|
337
|
+
* @param {string} jwt - JWT token containing transaction ID and user information
|
|
338
|
+
* Must include: `transactionId`, `identifierEmail`, `tenantId`
|
|
339
|
+
* @param {StatusCallback} [onStatusUpdate] - Optional callback for real-time status updates
|
|
340
|
+
* @returns {Promise<SubmissionResult>} Complete transaction result
|
|
341
|
+
*
|
|
342
|
+
* @throws {Error} 'Transaction signer not initialized' - Hook not ready
|
|
343
|
+
* @throws {Error} 'PERS Signer SDK not available' - SDK installation issue
|
|
344
|
+
* @throws {Error} 'Invalid or expired JWT token' - Token validation failed
|
|
345
|
+
* @throws {Error} 'Authentication failed' - WebAuthn authentication failed
|
|
346
|
+
* @throws {Error} 'Transaction signing failed' - Signing process failed
|
|
347
|
+
* @throws {Error} 'Transaction submission failed' - Blockchain submission failed
|
|
348
|
+
*
|
|
349
|
+
* @example
|
|
350
|
+
* **Basic Transaction:**
|
|
351
|
+
* ```typescript
|
|
352
|
+
* const { signAndSubmitTransactionWithJWT } = useTransactionSigner();
|
|
353
|
+
*
|
|
354
|
+
* const result = await signAndSubmitTransactionWithJWT(jwtToken);
|
|
355
|
+
* if (result.success) {
|
|
356
|
+
* console.log('Transaction Hash:', result.transactionHash);
|
|
357
|
+
* console.log('Blockchain explorer: [Chain-specific explorer URL]');
|
|
358
|
+
* }
|
|
359
|
+
* ```
|
|
360
|
+
*
|
|
361
|
+
* @example
|
|
362
|
+
* **With Status Updates:**
|
|
363
|
+
* ```typescript
|
|
364
|
+
* const result = await signAndSubmitTransactionWithJWT(jwtToken, (status, message) => {
|
|
365
|
+
* console.log(`Status: ${status} - ${message}`);
|
|
366
|
+
* setStatusText(message);
|
|
367
|
+
* });
|
|
368
|
+
* ```
|
|
369
|
+
*
|
|
370
|
+
* @example
|
|
371
|
+
* **With Error Handling:**
|
|
372
|
+
* ```typescript
|
|
373
|
+
* try {
|
|
374
|
+
* const result = await signAndSubmitTransactionWithJWT(redeemJWT);
|
|
375
|
+
*
|
|
376
|
+
* if (result.success) {
|
|
377
|
+
* // Transaction successful
|
|
378
|
+
* setTransactionHash(result.transactionHash);
|
|
379
|
+
*
|
|
380
|
+
* if (result.shouldRedirect && result.redirectUrl) {
|
|
381
|
+
* // Handle post-transaction redirect
|
|
382
|
+
* Linking.openURL(result.redirectUrl);
|
|
383
|
+
* }
|
|
384
|
+
* } else {
|
|
385
|
+
* // Transaction failed but didn't throw
|
|
386
|
+
* setError(result.error || 'Transaction failed');
|
|
387
|
+
* }
|
|
388
|
+
* } catch (error) {
|
|
389
|
+
* // Handle different error types
|
|
390
|
+
* if (error.message.includes('expired')) {
|
|
391
|
+
* // Token expired - redirect to login
|
|
392
|
+
* navigation.navigate('Login');
|
|
393
|
+
* } else if (error.message.includes('network')) {
|
|
394
|
+
* // Network error - suggest retry
|
|
395
|
+
* setError('Network error. Please try again.');
|
|
396
|
+
* } else {
|
|
397
|
+
* // Other errors
|
|
398
|
+
* setError(error.message);
|
|
399
|
+
* }
|
|
400
|
+
* }
|
|
401
|
+
* ```
|
|
402
|
+
*
|
|
403
|
+
* @see {@link SubmissionResult} for detailed result structure
|
|
404
|
+
*/
|
|
405
|
+
const signAndSubmitTransactionWithJWT = useCallback(async (jwt: string, onStatusUpdate?: OnStatusUpdateFn): Promise<SubmissionResult> => {
|
|
406
|
+
if (!isSignerInitialized || !signerSDKRef.current) {
|
|
407
|
+
throw new Error('Transaction signer not initialized');
|
|
408
|
+
}
|
|
409
|
+
if (!getSignerSDKFactory()) {
|
|
410
|
+
throw new Error('PERS Signer SDK not available. Blockchain signing is not supported.');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Create status callback wrapper that updates both hook state and calls user callback
|
|
414
|
+
const statusCallback: StatusCallback = {
|
|
415
|
+
onStatusUpdate: (status: SigningStatusType, message: string, data?: StatusUpdateData) => {
|
|
416
|
+
setCurrentStatus(status);
|
|
417
|
+
setStatusMessage(message);
|
|
418
|
+
onStatusUpdate?.(status, message, data);
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
// Use the actual SDK method that handles the complete sign + submit flow
|
|
424
|
+
const submissionResult = await signerSDKRef.current.signAndSubmitPersTransaction(jwt, statusCallback);
|
|
425
|
+
|
|
426
|
+
return submissionResult;
|
|
427
|
+
|
|
428
|
+
} catch (error) {
|
|
429
|
+
console.error('[useTransactionSigner] JWT transaction signing failed:', error);
|
|
430
|
+
const SigningStatusEnum = getSigningStatus();
|
|
431
|
+
if (SigningStatusEnum) setCurrentStatus(SigningStatusEnum.ERROR);
|
|
432
|
+
setStatusMessage(error instanceof Error ? error.message : 'Transaction failed');
|
|
433
|
+
throw error; // Re-throw to maintain error handling upstream
|
|
434
|
+
}
|
|
435
|
+
}, [isSignerInitialized]);
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Sign a PERS transaction without submitting (for POS flows with QR codes)
|
|
439
|
+
*
|
|
440
|
+
* This method signs the transaction but does NOT submit it to the blockchain.
|
|
441
|
+
* Instead, it returns the signature which can be displayed as a QR code for
|
|
442
|
+
* the business to scan and submit.
|
|
443
|
+
*
|
|
444
|
+
* **Use Case:** Point-of-Sale (POS) flows where:
|
|
445
|
+
* 1. User signs the transaction
|
|
446
|
+
* 2. App generates QR code with signature
|
|
447
|
+
* 3. Business scans QR code
|
|
448
|
+
* 4. Business submits transaction to blockchain
|
|
449
|
+
*
|
|
450
|
+
* @param {string} jwt - JWT token with transaction and user data
|
|
451
|
+
* @param {StatusCallback} [onStatusUpdate] - Optional callback for status updates
|
|
452
|
+
* @returns {Promise<TransactionSigningResult>} Signing result with signature for QR code
|
|
453
|
+
*
|
|
454
|
+
* @example
|
|
455
|
+
* ```typescript
|
|
456
|
+
* const { signPersTransaction } = useTransactionSigner();
|
|
457
|
+
*
|
|
458
|
+
* const result = await signPersTransaction(jwtToken);
|
|
459
|
+
* if (result.success) {
|
|
460
|
+
* const qrData = {
|
|
461
|
+
* transactionId: result.transactionId,
|
|
462
|
+
* signature: result.signature,
|
|
463
|
+
* transactionFormat: result.signingData.format,
|
|
464
|
+
* txType: 'PENDING_SUBMISSION'
|
|
465
|
+
* };
|
|
466
|
+
* // Display QR code with qrData
|
|
467
|
+
* }
|
|
468
|
+
* ```
|
|
469
|
+
*/
|
|
470
|
+
const signPersTransaction = useCallback(async (jwt: string, onStatusUpdate?: OnStatusUpdateFn): Promise<TransactionSigningResult> => {
|
|
471
|
+
if (!isSignerInitialized || !signerSDKRef.current) {
|
|
472
|
+
throw new Error('Signer SDK not initialized');
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Create status callback wrapper
|
|
476
|
+
const statusCallback: StatusCallback = {
|
|
477
|
+
onStatusUpdate: (status: SigningStatusType, message: string, data?: StatusUpdateData) => {
|
|
478
|
+
setCurrentStatus(status);
|
|
479
|
+
setStatusMessage(message);
|
|
480
|
+
onStatusUpdate?.(status, message, data);
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
try {
|
|
485
|
+
// Use the SDK's sign-only method
|
|
486
|
+
const signingResult = await signerSDKRef.current.signPersTransaction(jwt, statusCallback);
|
|
487
|
+
return signingResult;
|
|
488
|
+
} catch (error) {
|
|
489
|
+
console.error('[useTransactionSigner] Sign-only transaction failed:', error);
|
|
490
|
+
const SigningStatusEnum = getSigningStatus();
|
|
491
|
+
if (SigningStatusEnum) setCurrentStatus(SigningStatusEnum.ERROR);
|
|
492
|
+
setStatusMessage(error instanceof Error ? error.message : 'Signing failed');
|
|
493
|
+
throw error;
|
|
494
|
+
}
|
|
495
|
+
}, [isSignerInitialized]);
|
|
496
|
+
|
|
497
|
+
return {
|
|
498
|
+
/**
|
|
499
|
+
* Sign and submit a blockchain transaction using JWT token
|
|
500
|
+
*
|
|
501
|
+
* Main method for executing blockchain transactions. Handles complete flow
|
|
502
|
+
* from authentication to blockchain submission in a single call.
|
|
503
|
+
*
|
|
504
|
+
* @param {string} jwt - JWT token with transaction and user data
|
|
505
|
+
* @param {StatusCallback} [onStatusUpdate] - Optional callback for real-time status updates
|
|
506
|
+
* @returns {Promise<SubmissionResult>} Transaction result with hash and status
|
|
507
|
+
*/
|
|
508
|
+
signAndSubmitTransactionWithJWT,
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Sign a transaction without submitting (for POS flows)
|
|
512
|
+
*
|
|
513
|
+
* Signs the transaction and returns the signature without submitting to blockchain.
|
|
514
|
+
* Use this for Point-of-Sale flows where the business scans a QR code and submits.
|
|
515
|
+
*
|
|
516
|
+
* @param {string} jwt - JWT token with transaction and user data
|
|
517
|
+
* @param {StatusCallback} [onStatusUpdate] - Optional callback for status updates
|
|
518
|
+
* @returns {Promise<TransactionSigningResult>} Signing result with signature for QR code
|
|
519
|
+
*/
|
|
520
|
+
signPersTransaction,
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Whether the transaction signer SDK has been successfully initialized
|
|
524
|
+
*
|
|
525
|
+
* Use this to show loading states while the signer is being set up.
|
|
526
|
+
* When `false`, the signer is still initializing and transactions cannot be processed.
|
|
527
|
+
*
|
|
528
|
+
* @example
|
|
529
|
+
* ```typescript
|
|
530
|
+
* const { isSignerInitialized } = useTransactionSigner();
|
|
531
|
+
*
|
|
532
|
+
* if (!isSignerInitialized) {
|
|
533
|
+
* return <LoadingSpinner text="Initializing signer..." />;
|
|
534
|
+
* }
|
|
535
|
+
* ```
|
|
536
|
+
*/
|
|
537
|
+
isSignerInitialized,
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Whether blockchain transaction signing is fully available
|
|
541
|
+
*
|
|
542
|
+
* This combines multiple checks: SDK availability, initialization status,
|
|
543
|
+
* and configuration validity. Use this to enable/disable transaction buttons.
|
|
544
|
+
*
|
|
545
|
+
* Returns `true` only when:
|
|
546
|
+
* - PERS Signer SDK is installed and available
|
|
547
|
+
* - Signer has been successfully initialized
|
|
548
|
+
* - All required dependencies are loaded
|
|
549
|
+
*
|
|
550
|
+
* @example
|
|
551
|
+
* ```typescript
|
|
552
|
+
* const { isSignerAvailable } = useTransactionSigner();
|
|
553
|
+
*
|
|
554
|
+
* return (
|
|
555
|
+
* <TouchableOpacity
|
|
556
|
+
* disabled={!isSignerAvailable}
|
|
557
|
+
* style={[styles.button, !isSignerAvailable && styles.disabled]}
|
|
558
|
+
* >
|
|
559
|
+
* <Text>{isSignerAvailable ? 'Sign Transaction' : 'Signer Unavailable'}</Text>
|
|
560
|
+
* </TouchableOpacity>
|
|
561
|
+
* );
|
|
562
|
+
* ```
|
|
563
|
+
*/
|
|
564
|
+
isSignerAvailable: isSignerInitialized && !!getSignerSDKFactory(),
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Current signing status for UI feedback
|
|
568
|
+
*
|
|
569
|
+
* Tracks the current state of the signing process. Use this to show
|
|
570
|
+
* progress indicators or status messages during transaction signing.
|
|
571
|
+
*
|
|
572
|
+
* Possible values: 'initializing', 'authenticating', 'preparing',
|
|
573
|
+
* 'signing', 'submitting', 'completed', 'error', 'expired'
|
|
574
|
+
*
|
|
575
|
+
* @example
|
|
576
|
+
* ```typescript
|
|
577
|
+
* const { currentStatus, statusMessage } = useTransactionSigner();
|
|
578
|
+
*
|
|
579
|
+
* return (
|
|
580
|
+
* <View>
|
|
581
|
+
* {currentStatus && (
|
|
582
|
+
* <Text>Status: {statusMessage}</Text>
|
|
583
|
+
* )}
|
|
584
|
+
* </View>
|
|
585
|
+
* );
|
|
586
|
+
* ```
|
|
587
|
+
*/
|
|
588
|
+
currentStatus,
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Human-readable status message
|
|
592
|
+
*
|
|
593
|
+
* Provides a user-friendly description of the current signing status.
|
|
594
|
+
* Updates in real-time as the transaction progresses.
|
|
595
|
+
*/
|
|
596
|
+
statusMessage,
|
|
597
|
+
};
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Type exports for external usage
|
|
602
|
+
*
|
|
603
|
+
* These types are re-exported from the PERS Signer SDK for convenience
|
|
604
|
+
* when working with transaction signing in React Native applications.
|
|
605
|
+
*
|
|
606
|
+
* @see {@link SubmissionResult} - Complete transaction submission result
|
|
607
|
+
* @see {@link AuthenticatedUser} - User authentication data structure
|
|
608
|
+
* @see {@link TransactionSigningResult} - Transaction signing result details
|
|
609
|
+
* @see {@link TransactionSignerHook} - Hook return interface
|
|
610
|
+
*/
|
|
611
|
+
export type { SubmissionResult, AuthenticatedUser, TransactionSigningResult };
|