import { useCallback, useState, useRef, useEffect } from 'react'; // import { usePersSDK } from '../providers/PersSDKProvider'; // TYPE-ONLY imports from /types entry - NO native module deps, safe at any time import type { PersSignerConfig, StatusCallback, StatusUpdateData } from '@explorins/pers-signer/types'; import type { SigningStatus as SigningStatusType } from '@explorins/pers-signer/types'; // Runtime const import - also from /types (no native deps) import { SigningStatus as SigningStatusConst } from '@explorins/pers-signer/types'; // Import actual SDK types from the React Native-specific export type PersSignerSDK = import('@explorins/pers-signer/react-native').PersSignerSDK; type SubmissionResult = import('@explorins/pers-signer/react-native').SubmissionResult; type AuthenticatedUser = import('@explorins/pers-signer/react-native').AuthenticatedUser; type TransactionSigningResult = import('@explorins/pers-signer/react-native').TransactionSigningResult; // Re-export the type for external consumers export type { SigningStatusType as SigningStatus }; export type { StatusUpdateData }; /** * Simple status callback function type for React Native consumers * (Simplified from the signer SDK's object-based StatusCallback interface) */ export type OnStatusUpdateFn = (status: SigningStatusType, message: string, data?: StatusUpdateData) => void; // Lazy-loaded signer SDK - initialized on first use to avoid native module timing issues let _createPersSignerSDK: ((config: PersSignerConfig) => Promise) | null = null; let _signerInitAttempted = false; /** * Get the SigningStatus const object * Now directly imported from /types (no native deps, always available) */ function getSigningStatus(): typeof SigningStatusType { return SigningStatusConst; } /** * Lazily load the PERS Signer SDK. * * Supports customer pre-initialization via global.__PersSigner for environments * where native module timing is unpredictable (e.g., certain React Native setups). * * @returns The createPersSignerSDK function or null if unavailable */ function getSignerSDKFactory(): ((config: PersSignerConfig) => Promise) | null { if (_signerInitAttempted) { // Already tried - return cached result (could be null if failed) return _createPersSignerSDK; } _signerInitAttempted = true; // Check for customer pre-initialized global (for native module timing issues) if (typeof global !== 'undefined' && (global as any).__PersSigner?.createPersSignerSDK) { const globalModule = (global as any).__PersSigner; _createPersSignerSDK = globalModule.createPersSignerSDK; return _createPersSignerSDK; } // Standard lazy load try { const signerModule = require('@explorins/pers-signer/react-native'); _createPersSignerSDK = signerModule.createPersSignerSDK; return _createPersSignerSDK; } catch (error: unknown) { console.warn('[useTransactionSigner] PERS Signer SDK not available:', (error as Error).message); console.warn('[useTransactionSigner] Real blockchain signing will not be available'); console.warn('[useTransactionSigner] Tip: If timing issues, pre-initialize in index.js: global.__PersSigner = require("@explorins/pers-signer/react-native")'); _createPersSignerSDK = null; // Mark as unavailable return null; } } /** * Return interface for the useTransactionSigner hook * * Provides secure transaction signing capabilities for EVM blockchain interactions * and error handling for React Native applications. * * @interface TransactionSignerHook * @property {Function} signAndSubmitTransactionWithJWT - Main method to sign and submit transactions * @property {Function} signPersTransaction - Sign transaction without submitting (for POS flows) * @property {boolean} isSignerInitialized - Whether the signer SDK has been initialized * @property {boolean} isSignerAvailable - Whether signing functionality is fully available * @property {SigningStatus | null} currentStatus - Current signing status for UI feedback * @property {string | null} statusMessage - Human-readable status message */ export interface TransactionSignerHook { signAndSubmitTransactionWithJWT: (jwt: string, onStatusUpdate?: OnStatusUpdateFn) => Promise; signPersTransaction: (jwt: string, onStatusUpdate?: OnStatusUpdateFn) => Promise; isSignerInitialized: boolean; isSignerAvailable: boolean; currentStatus: SigningStatusType | null; statusMessage: string | null; } // Constants - TODO: Move to environment config later const DEFAULT_ETHERS_PROVIDER = "https://sepolia.infura.io/v3/2781b4b5242343d5b0954c98f287b29e"; /** * React Native hook for blockchain transaction signing using PERS Signer SDK * * This hook provides a complete blockchain transaction signing solution for React Native * applications, integrating with the PERS ecosystem for tourism loyalty and reward systems. * It automatically handles WebAuthn authentication, transaction preparation, signing, and * blockchain submission in a single convenient interface. * * **Features:** * - Automatic WebAuthn provider initialization for React Native * - 5-minute authentication caching to reduce repeated logins * - Complete transaction lifecycle management (sign + submit) * - Comprehensive error handling with detailed error messages * - Real-time status monitoring for UI feedback * - Automatic retry logic for network failures * * **Underlying SDK Methods:** * The hook uses the PERS Signer SDK which provides 5 core methods: * 1. `loginUser(jwtToken)` - Authenticate user with 5-minute caching * 2. `signTransaction(signingData, jwtToken)` - Sign transactions with auto-login * 3. `submitTransaction(signingResult, jwtToken)` - Submit signed transactions * 4. `signPersTransaction(jwtToken)` - Legacy one-liner for backward compatibility * 5. `signAndSubmitPersTransaction(jwtToken)` - Complete sign + submit flow (used by this hook) * * **Security:** * - WebAuthn-based secure authentication (no passwords stored) * - JWT token validation and expiration checking * - Secure transaction signing using device biometrics/PIN * - No sensitive data stored locally * * @returns {TransactionSignerHook} Hook interface with signing methods and status * * @example * **Basic Usage:** * ```typescript * import { useTransactionSigner } from '@explorins/pers-sdk-react-native'; * * function TransactionScreen() { * const { * signAndSubmitTransactionWithJWT, * isSignerAvailable, * isSignerInitialized * } = useTransactionSigner(); * * const handleSign = async (jwtFromRedemption: string) => { * if (!isSignerAvailable) { * console.error('Signer not available'); * return; * } * * try { * const result = await signAndSubmitTransactionWithJWT(jwtFromRedemption); * if (result.success) { * console.log('Transaction completed:', result.transactionHash); * // Handle successful transaction * if (result.shouldRedirect && result.redirectUrl) { * // Navigate to success page or external URL * } * } * } catch (error) { * console.error('Transaction failed:', error.message); * // Handle error (show user-friendly message) * } * }; * * return ( * * handleSign(redeemJWT)} * disabled={!isSignerAvailable} * style={[styles.button, !isSignerAvailable && styles.disabled]} * > * * {isSignerInitialized ? 'Sign Transaction' : 'Initializing...'} * * * * ); * } * ``` * * @example * **Advanced Usage with Error Handling:** * ```typescript * function AdvancedTransactionComponent() { * const [isLoading, setIsLoading] = useState(false); * const [error, setError] = useState(null); * const { signAndSubmitTransactionWithJWT, isSignerAvailable } = useTransactionSigner(); * * const handleTransaction = async (jwt: string) => { * setIsLoading(true); * setError(null); * * try { * const result = await signAndSubmitTransactionWithJWT(jwt); * * if (result.success) { * // Success handling * Alert.alert( * 'Success', * `Transaction completed!\nHash: ${result.transactionHash}` * ); * } else { * throw new Error(result.error || 'Transaction failed'); * } * } catch (err) { * const errorMessage = err instanceof Error ? err.message : 'Unknown error'; * setError(errorMessage); * * // Different error handling based on error type * if (errorMessage.includes('expired')) { * Alert.alert('Session Expired', 'Please log in again'); * } else if (errorMessage.includes('network')) { * Alert.alert('Network Error', 'Please check your connection'); * } else { * Alert.alert('Transaction Failed', errorMessage); * } * } finally { * setIsLoading(false); * } * }; * * return ( * * {error && ( * {error} * )} * handleTransaction(jwtToken)} * disabled={!isSignerAvailable || isLoading} * > * * {isLoading ? 'Processing...' : 'Sign & Submit Transaction'} * * * * ); * } * ``` * * @example * **Integration with PERS SDK:** * ```typescript * import { usePersSDK } from '@explorins/pers-sdk-react-native'; * import { useTransactionSigner } from '@explorins/pers-sdk-react-native'; * * function RedemptionFlow() { * const { user, redeemTokens } = usePersSDK(); * const { signAndSubmitTransactionWithJWT } = useTransactionSigner(); * * const handleRedemption = async (tokenAmount: number) => { * try { * // Step 1: Create redemption with PERS SDK * const redemption = await redeemTokens({ * tokenAmount, * destinationAddress: user?.walletAddress * }); * * // Step 2: Sign and submit transaction * const txResult = await signAndSubmitTransactionWithJWT(redemption.jwtToken); * * if (txResult.success) { * console.log('Redemption completed:', txResult.transactionHash); * } * } catch (error) { * console.error('Redemption failed:', error); * } * }; * } * ``` * * @see {@link SubmissionResult} for transaction result structure * @see {@link TransactionSigningResult} for signing result details * @see {@link AuthenticatedUser} for user authentication data * * @since 1.5.0 */ export const useTransactionSigner = (): TransactionSignerHook => { // const { isInitialized, isAuthenticated, user } = usePersSDK(); const [isSignerInitialized, setIsSignerInitialized] = useState(false); const [currentStatus, setCurrentStatus] = useState(null); const [statusMessage, setStatusMessage] = useState(null); const signerSDKRef = useRef(null); // Auto-initialize signer SDK when PERS SDK is ready useEffect(() => { const initializeSignerSDK = async () => { const createPersSignerSDK = getSignerSDKFactory(); if (!createPersSignerSDK) { console.warn('[useTransactionSigner] Signer SDK not available'); return; } try { // React Native createPersSignerSDK automatically includes WebAuthn provider const signerSDK = await createPersSignerSDK({ ethersProviderUrl: DEFAULT_ETHERS_PROVIDER, relyingPartyName: 'PERS React Native App' }); signerSDKRef.current = signerSDK; setIsSignerInitialized(true); } catch (error) { console.error('[useTransactionSigner] Failed to initialize signer SDK:', error); } }; if (!isSignerInitialized) { initializeSignerSDK(); } }, [isSignerInitialized]); /** * Sign and submit a blockchain transaction using JWT token * * This is the main method for executing blockchain transactions in React Native. * It handles the complete transaction lifecycle: authentication, signing, and * blockchain submission in a single call. * * **Process Flow:** * 1. Validates the JWT token and extracts transaction data * 2. Authenticates the user using WebAuthn (cached for 5 minutes) * 3. Fetches transaction data from PERS backend * 4. Signs the transaction using device security (biometrics/PIN) * 5. Submits the signed transaction to the blockchain * 6. Returns detailed result with transaction hash and status * * **Security Features:** * - JWT token validation and expiration checking * - WebAuthn authentication with device biometrics * - Secure transaction signing without exposing private keys * - Automatic session management with secure caching * * @param {string} jwt - JWT token containing transaction ID and user information * Must include: `transactionId`, `identifierEmail`, `tenantId` * @param {StatusCallback} [onStatusUpdate] - Optional callback for real-time status updates * @returns {Promise} Complete transaction result * * @throws {Error} 'Transaction signer not initialized' - Hook not ready * @throws {Error} 'PERS Signer SDK not available' - SDK installation issue * @throws {Error} 'Invalid or expired JWT token' - Token validation failed * @throws {Error} 'Authentication failed' - WebAuthn authentication failed * @throws {Error} 'Transaction signing failed' - Signing process failed * @throws {Error} 'Transaction submission failed' - Blockchain submission failed * * @example * **Basic Transaction:** * ```typescript * const { signAndSubmitTransactionWithJWT } = useTransactionSigner(); * * const result = await signAndSubmitTransactionWithJWT(jwtToken); * if (result.success) { * console.log('Transaction Hash:', result.transactionHash); * console.log('Blockchain explorer: [Chain-specific explorer URL]'); * } * ``` * * @example * **With Status Updates:** * ```typescript * const result = await signAndSubmitTransactionWithJWT(jwtToken, (status, message) => { * console.log(`Status: ${status} - ${message}`); * setStatusText(message); * }); * ``` * * @example * **With Error Handling:** * ```typescript * try { * const result = await signAndSubmitTransactionWithJWT(redeemJWT); * * if (result.success) { * // Transaction successful * setTransactionHash(result.transactionHash); * * if (result.shouldRedirect && result.redirectUrl) { * // Handle post-transaction redirect * Linking.openURL(result.redirectUrl); * } * } else { * // Transaction failed but didn't throw * setError(result.error || 'Transaction failed'); * } * } catch (error) { * // Handle different error types * if (error.message.includes('expired')) { * // Token expired - redirect to login * navigation.navigate('Login'); * } else if (error.message.includes('network')) { * // Network error - suggest retry * setError('Network error. Please try again.'); * } else { * // Other errors * setError(error.message); * } * } * ``` * * @see {@link SubmissionResult} for detailed result structure */ const signAndSubmitTransactionWithJWT = useCallback(async (jwt: string, onStatusUpdate?: OnStatusUpdateFn): Promise => { if (!isSignerInitialized || !signerSDKRef.current) { throw new Error('Transaction signer not initialized'); } if (!getSignerSDKFactory()) { throw new Error('PERS Signer SDK not available. Blockchain signing is not supported.'); } // Create status callback wrapper that updates both hook state and calls user callback const statusCallback: StatusCallback = { onStatusUpdate: (status: SigningStatusType, message: string, data?: StatusUpdateData) => { setCurrentStatus(status); setStatusMessage(message); onStatusUpdate?.(status, message, data); } }; try { // Use the actual SDK method that handles the complete sign + submit flow const submissionResult = await signerSDKRef.current.signAndSubmitPersTransaction(jwt, statusCallback); return submissionResult; } catch (error) { console.error('[useTransactionSigner] JWT transaction signing failed:', error); const SigningStatusEnum = getSigningStatus(); if (SigningStatusEnum) setCurrentStatus(SigningStatusEnum.ERROR); setStatusMessage(error instanceof Error ? error.message : 'Transaction failed'); throw error; // Re-throw to maintain error handling upstream } }, [isSignerInitialized]); /** * Sign a PERS transaction without submitting (for POS flows with QR codes) * * This method signs the transaction but does NOT submit it to the blockchain. * Instead, it returns the signature which can be displayed as a QR code for * the business to scan and submit. * * **Use Case:** Point-of-Sale (POS) flows where: * 1. User signs the transaction * 2. App generates QR code with signature * 3. Business scans QR code * 4. Business submits transaction to blockchain * * @param {string} jwt - JWT token with transaction and user data * @param {StatusCallback} [onStatusUpdate] - Optional callback for status updates * @returns {Promise} Signing result with signature for QR code * * @example * ```typescript * const { signPersTransaction } = useTransactionSigner(); * * const result = await signPersTransaction(jwtToken); * if (result.success) { * const qrData = { * transactionId: result.transactionId, * signature: result.signature, * transactionFormat: result.signingData.format, * txType: 'PENDING_SUBMISSION' * }; * // Display QR code with qrData * } * ``` */ const signPersTransaction = useCallback(async (jwt: string, onStatusUpdate?: OnStatusUpdateFn): Promise => { if (!isSignerInitialized || !signerSDKRef.current) { throw new Error('Signer SDK not initialized'); } // Create status callback wrapper const statusCallback: StatusCallback = { onStatusUpdate: (status: SigningStatusType, message: string, data?: StatusUpdateData) => { setCurrentStatus(status); setStatusMessage(message); onStatusUpdate?.(status, message, data); } }; try { // Use the SDK's sign-only method const signingResult = await signerSDKRef.current.signPersTransaction(jwt, statusCallback); return signingResult; } catch (error) { console.error('[useTransactionSigner] Sign-only transaction failed:', error); const SigningStatusEnum = getSigningStatus(); if (SigningStatusEnum) setCurrentStatus(SigningStatusEnum.ERROR); setStatusMessage(error instanceof Error ? error.message : 'Signing failed'); throw error; } }, [isSignerInitialized]); return { /** * Sign and submit a blockchain transaction using JWT token * * Main method for executing blockchain transactions. Handles complete flow * from authentication to blockchain submission in a single call. * * @param {string} jwt - JWT token with transaction and user data * @param {StatusCallback} [onStatusUpdate] - Optional callback for real-time status updates * @returns {Promise} Transaction result with hash and status */ signAndSubmitTransactionWithJWT, /** * Sign a transaction without submitting (for POS flows) * * Signs the transaction and returns the signature without submitting to blockchain. * Use this for Point-of-Sale flows where the business scans a QR code and submits. * * @param {string} jwt - JWT token with transaction and user data * @param {StatusCallback} [onStatusUpdate] - Optional callback for status updates * @returns {Promise} Signing result with signature for QR code */ signPersTransaction, /** * Whether the transaction signer SDK has been successfully initialized * * Use this to show loading states while the signer is being set up. * When `false`, the signer is still initializing and transactions cannot be processed. * * @example * ```typescript * const { isSignerInitialized } = useTransactionSigner(); * * if (!isSignerInitialized) { * return ; * } * ``` */ isSignerInitialized, /** * Whether blockchain transaction signing is fully available * * This combines multiple checks: SDK availability, initialization status, * and configuration validity. Use this to enable/disable transaction buttons. * * Returns `true` only when: * - PERS Signer SDK is installed and available * - Signer has been successfully initialized * - All required dependencies are loaded * * @example * ```typescript * const { isSignerAvailable } = useTransactionSigner(); * * return ( * * {isSignerAvailable ? 'Sign Transaction' : 'Signer Unavailable'} * * ); * ``` */ isSignerAvailable: isSignerInitialized && !!getSignerSDKFactory(), /** * Current signing status for UI feedback * * Tracks the current state of the signing process. Use this to show * progress indicators or status messages during transaction signing. * * Possible values: 'initializing', 'authenticating', 'preparing', * 'signing', 'submitting', 'completed', 'error', 'expired' * * @example * ```typescript * const { currentStatus, statusMessage } = useTransactionSigner(); * * return ( * * {currentStatus && ( * Status: {statusMessage} * )} * * ); * ``` */ currentStatus, /** * Human-readable status message * * Provides a user-friendly description of the current signing status. * Updates in real-time as the transaction progresses. */ statusMessage, }; }; /** * Type exports for external usage * * These types are re-exported from the PERS Signer SDK for convenience * when working with transaction signing in React Native applications. * * @see {@link SubmissionResult} - Complete transaction submission result * @see {@link AuthenticatedUser} - User authentication data structure * @see {@link TransactionSigningResult} - Transaction signing result details * @see {@link TransactionSignerHook} - Hook return interface */ export type { SubmissionResult, AuthenticatedUser, TransactionSigningResult };