UNPKG

@cardql/react-native-tap

Version:

CardQL SDK for React Native tap-to-pay for secure in-person payments

1,548 lines (1,538 loc) 48.2 kB
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) { if (typeof require !== "undefined") return require.apply(this, arguments); throw Error('Dynamic require of "' + x + '" is not supported'); }); // src/index.ts export * from "@cardql/core"; export * from "@cardql/react-native"; // src/hooks/useTapToPay.ts import { useState, useCallback, useEffect, useRef } from "react"; import { useCardQLClient } from "@cardql/react-native"; // src/utils/nfcManager.ts import { Platform } from "react-native"; var StripeTerminal; var useStripeTerminal; try { const stripeTerminal = __require("@stripe/stripe-terminal-react-native"); StripeTerminal = stripeTerminal.StripeTerminal; useStripeTerminal = stripeTerminal.useStripeTerminal; } catch (error) { console.warn("Stripe Terminal SDK not found, using mock implementation"); StripeTerminal = { initialize: () => Promise.resolve(), discoverReaders: () => Promise.resolve({ readers: [] }), connectLocalMobileReader: () => Promise.resolve({ reader: {} }), connectBluetoothReader: () => Promise.resolve({ reader: {} }), disconnectReader: () => Promise.resolve(), createPaymentIntent: () => Promise.resolve({ paymentIntent: {} }), collectPaymentMethod: () => Promise.resolve({ paymentIntent: {} }), confirmPaymentIntent: () => Promise.resolve({ paymentIntent: {} }), cancelCollectPaymentMethod: () => Promise.resolve(), isInitialized: () => false }; useStripeTerminal = () => ({ initialize: StripeTerminal.initialize, discoverReaders: StripeTerminal.discoverReaders, connectReader: StripeTerminal.connectLocalMobileReader, disconnectReader: StripeTerminal.disconnectReader, createPaymentIntent: StripeTerminal.createPaymentIntent, collectPaymentMethod: StripeTerminal.collectPaymentMethod, confirmPaymentIntent: StripeTerminal.confirmPaymentIntent, cancelCollectPaymentMethod: StripeTerminal.cancelCollectPaymentMethod, connectedReader: null, discoveredReaders: [], isInitialized: false }); } var StripeTerminalManager = class { isInitialized = false; config; connectedReader; discoveredReaders = []; currentPaymentIntent; async initialize(config) { if (this.isInitialized) return; try { this.config = config; await StripeTerminal.initialize({ tokenProvider: config.tokenProvider, logLevel: config.logLevel || "info" }); this.isInitialized = true; } catch (error) { throw this.createError( "TERMINAL_NOT_INITIALIZED", `Failed to initialize Stripe Terminal: ${error.message}`, error ); } } async checkCapabilities() { const isSupported = await this.isTerminalSupported(); return { isNFCSupported: isSupported, isNFCEnabled: isSupported, canProcessPayments: isSupported && this.isInitialized, supportedCardTypes: this.getSupportedCardTypes(), maxTransactionAmount: this.getMaxTransactionAmount(), supportsLocalMobile: Platform.OS === "ios" || Platform.OS === "android", supportsBluetoothReaders: true }; } async getDeviceCapabilities() { const nfcCapabilities = await this.checkCapabilities(); return { nfc: nfcCapabilities, hasSecureElement: this.hasSecureElement(), supportsTapToPay: nfcCapabilities.canProcessPayments, supportsStripeTerminal: await this.isTerminalSupported(), deviceModel: this.getDeviceModel(), osVersion: Platform.Version.toString(), stripeTerminalVersion: this.getStripeTerminalVersion() }; } async discoverReaders(config) { if (!this.isInitialized) { throw this.createError( "TERMINAL_NOT_INITIALIZED", "Terminal not initialized" ); } try { const { readers, error } = await StripeTerminal.discoverReaders({ discoveryMethod: config.discoveryMethod, simulated: config.simulated || false, locationId: config.locationId }); if (error) { throw this.createError( "READER_DISCOVERY_FAILED", `Reader discovery failed: ${error.message}`, error ); } this.discoveredReaders = readers || []; return this.discoveredReaders; } catch (error) { if (error.code) throw error; throw this.createError( "READER_DISCOVERY_FAILED", `Failed to discover readers: ${error.message}`, error ); } } async connectReader(reader, config) { if (!this.isInitialized) { throw this.createError( "TERMINAL_NOT_INITIALIZED", "Terminal not initialized" ); } try { let result2; if (config?.discoveryMethod === "localMobile") { result2 = await StripeTerminal.connectLocalMobileReader({ reader, locationId: config?.locationId }); } else { result2 = await StripeTerminal.connectBluetoothReader({ reader, locationId: config?.locationId }); } if (result2.error) { throw this.createError( "READER_CONNECTION_FAILED", `Reader connection failed: ${result2.error.message}`, result2.error ); } this.connectedReader = result2.reader; return result2.reader; } catch (error) { if (error.code) throw error; throw this.createError( "READER_CONNECTION_FAILED", `Failed to connect reader: ${error.message}`, error ); } } async disconnectReader() { if (!this.connectedReader) return; try { await StripeTerminal.disconnectReader(); this.connectedReader = void 0; } catch (error) { throw this.createError( "READER_CONNECTION_FAILED", `Failed to disconnect reader: ${error.message}`, error ); } } async createPaymentIntent(amount, currency, metadata) { if (!this.isInitialized) { throw this.createError( "TERMINAL_NOT_INITIALIZED", "Terminal not initialized" ); } try { const { paymentIntent, error } = await StripeTerminal.createPaymentIntent( { amount, currency: currency.toLowerCase(), paymentMethodTypes: ["card_present"], captureMethod: "automatic", metadata: metadata || {} } ); if (error) { throw this.createError( "PAYMENT_INTENT_CREATION_FAILED", `Payment intent creation failed: ${error.message}`, error ); } this.currentPaymentIntent = paymentIntent; return paymentIntent; } catch (error) { if (error.code) throw error; throw this.createError( "PAYMENT_INTENT_CREATION_FAILED", `Failed to create payment intent: ${error.message}`, error ); } } async collectPaymentMethod(paymentIntent) { if (!this.connectedReader) { throw this.createError("READER_CONNECTION_FAILED", "No reader connected"); } try { const { paymentIntent: updatedPaymentIntent, error } = await StripeTerminal.collectPaymentMethod({ paymentIntent }); if (error) { throw this.createError( "PAYMENT_COLLECTION_FAILED", `Payment collection failed: ${error.message}`, error ); } return updatedPaymentIntent; } catch (error) { if (error.code) throw error; throw this.createError( "PAYMENT_COLLECTION_FAILED", `Failed to collect payment method: ${error.message}`, error ); } } async confirmPaymentIntent(paymentIntent) { try { const { paymentIntent: confirmedPaymentIntent, error } = await StripeTerminal.confirmPaymentIntent({ paymentIntent }); if (error) { throw this.createError( "PAYMENT_CONFIRMATION_FAILED", `Payment confirmation failed: ${error.message}`, error ); } return confirmedPaymentIntent; } catch (error) { if (error.code) throw error; throw this.createError( "PAYMENT_CONFIRMATION_FAILED", `Failed to confirm payment intent: ${error.message}`, error ); } } async cancelPayment() { try { await StripeTerminal.cancelCollectPaymentMethod(); } catch (error) { throw this.createError( "PAYMENT_COLLECTION_FAILED", `Failed to cancel payment: ${error.message}`, error ); } } getConnectedReader() { return this.connectedReader; } getDiscoveredReaders() { return this.discoveredReaders; } getCurrentPaymentIntent() { return this.currentPaymentIntent; } // Private helper methods async isTerminalSupported() { try { return Platform.OS === "ios" || Platform.OS === "android"; } catch { return false; } } getSupportedCardTypes() { return [ "visa", "mastercard", "amex", "discover", "diners", "jcb", "unionpay", "contactless" ]; } getMaxTransactionAmount() { return "999999.99"; } hasSecureElement() { return Platform.OS === "ios" || Platform.OS === "android"; } getDeviceModel() { return Platform.OS === "ios" ? "iPhone" : "Android Device"; } getStripeTerminalVersion() { return "0.0.1-beta.25"; } detectCardType(paymentMethod) { if (!paymentMethod?.card?.brand) return "unknown"; const brand = paymentMethod.card.brand.toLowerCase(); const cardTypeMap = { visa: "visa", mastercard: "mastercard", amex: "amex", discover: "discover", diners: "diners", jcb: "jcb", unionpay: "unionpay" }; return cardTypeMap[brand] || "unknown"; } detectPaymentMethod(paymentMethod) { if (!paymentMethod?.type) return "unknown"; switch (paymentMethod.type) { case "card_present": return "card_present"; default: return "contactless_card"; } } getLastFourDigits(paymentMethod) { return paymentMethod?.card?.last4 || "0000"; } generateTransactionId() { return `stripe_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } createError(code, message, details) { return { code, message, details, stripeError: details, userFriendlyMessage: this.getUserFriendlyMessage(code), canRetry: this.canRetryError(code), suggestedAction: this.getSuggestedAction(code) }; } getUserFriendlyMessage(code) { const messages = { TERMINAL_NOT_SUPPORTED: "This device doesn't support tap-to-pay", TERMINAL_NOT_INITIALIZED: "Payment system is not ready", READER_CONNECTION_FAILED: "Could not connect to card reader", READER_DISCOVERY_FAILED: "Could not find card readers", PAYMENT_INTENT_CREATION_FAILED: "Could not initialize payment", PAYMENT_COLLECTION_FAILED: "Could not process payment method", PAYMENT_CONFIRMATION_FAILED: "Could not confirm payment", CARD_READ_ERROR: "Unable to read the card. Please try again", CARD_NOT_SUPPORTED: "This card type is not accepted", TRANSACTION_DECLINED: "The transaction was declined", TRANSACTION_TIMEOUT: "The transaction timed out. Please try again", TRANSACTION_CANCELLED: "The transaction was cancelled", AMOUNT_TOO_HIGH: "The amount exceeds the transaction limit", AMOUNT_TOO_LOW: "The minimum transaction amount is not met", DEVICE_NOT_COMPATIBLE: "This device is not compatible with tap-to-pay", SECURITY_ERROR: "A security error occurred during the transaction", NETWORK_ERROR: "Network connection is required for this transaction", VALIDATION_ERROR: "Please check the transaction details", STRIPE_ERROR: "Payment processing error", UNKNOWN_ERROR: "An unexpected error occurred" }; return messages[code] || "An error occurred"; } canRetryError(code) { const retryableErrors = [ "READER_CONNECTION_FAILED", "READER_DISCOVERY_FAILED", "PAYMENT_COLLECTION_FAILED", "CARD_READ_ERROR", "TRANSACTION_TIMEOUT", "NETWORK_ERROR", "STRIPE_ERROR", "UNKNOWN_ERROR" ]; return retryableErrors.includes(code); } getSuggestedAction(code) { const actions = { TERMINAL_NOT_SUPPORTED: "Use a compatible device", TERMINAL_NOT_INITIALIZED: "Restart the app", READER_CONNECTION_FAILED: "Check reader connection", READER_DISCOVERY_FAILED: "Ensure reader is nearby and powered on", PAYMENT_INTENT_CREATION_FAILED: "Check payment settings", PAYMENT_COLLECTION_FAILED: "Try tapping the card again", PAYMENT_CONFIRMATION_FAILED: "Contact support", CARD_READ_ERROR: "Hold the card closer to the reader", CARD_NOT_SUPPORTED: "Try a different card", TRANSACTION_DECLINED: "Contact your card issuer", TRANSACTION_TIMEOUT: "Try the transaction again", TRANSACTION_CANCELLED: "Start a new transaction", AMOUNT_TOO_HIGH: "Reduce the transaction amount", AMOUNT_TOO_LOW: "Increase the transaction amount", DEVICE_NOT_COMPATIBLE: "Use a compatible device", SECURITY_ERROR: "Contact support", NETWORK_ERROR: "Check your internet connection", VALIDATION_ERROR: "Verify transaction details", STRIPE_ERROR: "Try again or contact support", UNKNOWN_ERROR: "Try again or contact support" }; return actions[code] || "Try again"; } }; var stripeTerminalManager = new StripeTerminalManager(); // src/hooks/useTapToPay.ts var useStripeTerminal2; try { useStripeTerminal2 = __require("@stripe/stripe-terminal-react-native").useStripeTerminal; } catch (error) { useStripeTerminal2 = () => ({ initialize: () => Promise.resolve(), discoverReaders: () => Promise.resolve({ readers: [] }), connectReader: () => Promise.resolve({ reader: {} }), disconnectReader: () => Promise.resolve(), createPaymentIntent: () => Promise.resolve({ paymentIntent: {} }), collectPaymentMethod: () => Promise.resolve({ paymentIntent: {} }), confirmPaymentIntent: () => Promise.resolve({ paymentIntent: {} }), cancelCollectPaymentMethod: () => Promise.resolve(), connectedReader: null, discoveredReaders: [], isInitialized: false }); } var DEFAULT_CONFIG = { merchantID: "", currency: "USD", timeout: 3e4, acceptedCardTypes: ["visa", "mastercard", "amex", "discover"], requireSignature: false, requirePin: false, enableTips: false, enablePartialPayments: false, skipReceipt: false, stripeConfig: { tokenProvider: async () => { throw new Error("Token provider must be implemented"); }, logLevel: "info", simulated: false }, captureMethod: "automatic" }; var DEFAULT_READER_CONFIG = { discoveryMethod: "localMobile", simulated: false, autoConnect: false, autoStart: false, vibrationEnabled: true, soundEnabled: true, animationEnabled: true, showAmount: true, showInstructions: true, instructionText: "Hold your card near the device", successMessage: "Payment successful", errorMessage: "Payment failed", enableTipping: false, skipTipping: true }; function useTapToPay(options = {}) { const { config = DEFAULT_CONFIG, readerConfig = DEFAULT_READER_CONFIG, events = {}, autoInit = false, autoDiscoverReaders = false, autoConnectReader = false } = options; const cardql = useCardQLClient(); const [isSupported, setIsSupported] = useState(false); const [isInitialized, setIsInitialized] = useState(false); const [isDiscoveringReaders, setIsDiscoveringReaders] = useState(false); const [discoveredReaders, setDiscoveredReaders] = useState([]); const [connectedReader, setConnectedReader] = useState(); const [isCollectingPayment, setIsCollectingPayment] = useState(false); const [currentTransaction, setCurrentTransaction] = useState(); const [deviceCapabilities, setDeviceCapabilities] = useState(); const [lastError, setLastError] = useState(); const timeoutRef = useRef(); const transactionIdRef = useRef(); const currentPaymentIntentRef = useRef(); useEffect(() => { const initCapabilities = async () => { try { const capabilities = await stripeTerminalManager.getDeviceCapabilities(); setDeviceCapabilities(capabilities); setIsSupported(capabilities.supportsStripeTerminal); if (autoInit && capabilities.supportsStripeTerminal) { await initialize(); } } catch (error) { setLastError(error); } }; initCapabilities(); }, [autoInit]); useEffect(() => { if (autoDiscoverReaders && isInitialized && !isDiscoveringReaders && discoveredReaders.length === 0) { discoverReaders(); } }, [ autoDiscoverReaders, isInitialized, isDiscoveringReaders, discoveredReaders.length ]); useEffect(() => { if (autoConnectReader && discoveredReaders.length > 0 && !connectedReader) { connectReader(discoveredReaders[0]); } }, [autoConnectReader, discoveredReaders, connectedReader]); useEffect(() => { return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } cancelPayment(); }; }, []); const initialize = useCallback(async () => { if (isInitialized) return; try { await stripeTerminalManager.initialize(config.stripeConfig); setIsInitialized(true); setLastError(void 0); } catch (error) { setLastError(error); throw error; } }, [isInitialized, config.stripeConfig]); const discoverReaders = useCallback( async (discoveryConfig) => { if (!isInitialized) { await initialize(); } if (isDiscoveringReaders) { return; } try { setIsDiscoveringReaders(true); setLastError(void 0); const readers = await stripeTerminalManager.discoverReaders( discoveryConfig || readerConfig ); setDiscoveredReaders(readers); events.onReaderDiscovered?.(readers); } catch (error) { setLastError(error); events.onError?.(error); throw error; } finally { setIsDiscoveringReaders(false); } }, [isInitialized, isDiscoveringReaders, readerConfig, events, initialize] ); const connectReader = useCallback( async (reader) => { if (!isInitialized) { await initialize(); } try { setLastError(void 0); const connectedReader2 = await stripeTerminalManager.connectReader( reader, readerConfig ); setConnectedReader(connectedReader2); events.onReaderConnected?.(connectedReader2); } catch (error) { setLastError(error); events.onError?.(error); throw error; } }, [isInitialized, readerConfig, events, initialize] ); const disconnectReader = useCallback(async () => { try { await stripeTerminalManager.disconnectReader(); setConnectedReader(void 0); events.onReaderDisconnected?.(); } catch (error) { setLastError(error); events.onError?.(error); throw error; } }, [events]); const createPaymentIntent = useCallback( async (input) => { if (!isInitialized) { throw new Error("Terminal not initialized"); } try { const amount = Math.round(parseFloat(input.amount) * 100); const paymentIntent = await stripeTerminalManager.createPaymentIntent( amount, input.currency, { merchant_id: input.merchantID, user_id: input.userID, description: input.description, ...input.metadata } ); currentPaymentIntentRef.current = paymentIntent; events.onPaymentIntentCreated?.(paymentIntent); return paymentIntent; } catch (error) { setLastError(error); events.onError?.(error); throw error; } }, [isInitialized, events] ); const collectPaymentMethod = useCallback( async (paymentIntent) => { if (!connectedReader) { throw new Error("No reader connected"); } try { setIsCollectingPayment(true); if (currentTransaction) { const collectingTransaction = { ...currentTransaction, state: "processing_payment_method" }; setCurrentTransaction(collectingTransaction); events.onTransactionStateChange?.( "processing_payment_method", collectingTransaction ); } const updatedPaymentIntent = await stripeTerminalManager.collectPaymentMethod(paymentIntent); const paymentMethod = updatedPaymentIntent.charges?.data?.[0]?.payment_method; if (paymentMethod) { events.onPaymentMethodCollected?.(paymentMethod); } events.onPaymentIntentUpdated?.(updatedPaymentIntent); return updatedPaymentIntent; } catch (error) { setLastError(error); events.onError?.(error); throw error; } finally { setIsCollectingPayment(false); } }, [connectedReader, currentTransaction, events] ); const confirmPayment = useCallback( async (paymentIntent) => { try { if (currentTransaction) { const confirmingTransaction = { ...currentTransaction, state: "capturing_payment" }; setCurrentTransaction(confirmingTransaction); events.onTransactionStateChange?.( "capturing_payment", confirmingTransaction ); } const confirmedPaymentIntent = await stripeTerminalManager.confirmPaymentIntent(paymentIntent); const payment = await cardql.api.createPayment({ amount: (confirmedPaymentIntent.amount / 100).toString(), currency: confirmedPaymentIntent.currency.toUpperCase(), merchantID: config.merchantID, description: `Stripe Terminal payment - ${confirmedPaymentIntent.id}`, metadata: { ...confirmedPaymentIntent.metadata, stripe_payment_intent_id: confirmedPaymentIntent.id, stripe_charge_id: confirmedPaymentIntent.charges?.data?.[0]?.id, payment_method_id: confirmedPaymentIntent.charges?.data?.[0]?.payment_method, reader_id: connectedReader?.id } }); if (currentTransaction) { const successTransaction = { ...currentTransaction, state: "success", stripePaymentIntentId: confirmedPaymentIntent.id, stripePaymentIntent: confirmedPaymentIntent }; setCurrentTransaction(successTransaction); const receiptData = generateReceiptData( payment, successTransaction, confirmedPaymentIntent ); const result2 = { success: true, payment, transactionData: successTransaction, stripePaymentIntent: confirmedPaymentIntent, receiptData }; events.onTransactionStateChange?.("success", successTransaction); events.onSuccess?.(result2); return result2; } return result; } catch (error) { const failedTransaction = { ...currentTransaction, state: "failed" }; setCurrentTransaction(failedTransaction); const tapError = { code: "PAYMENT_CONFIRMATION_FAILED", message: error.message || "Payment confirmation failed", details: error, stripeError: error, userFriendlyMessage: "The payment could not be completed", canRetry: true, suggestedAction: "Try the payment again" }; const result2 = { success: false, error: tapError, transactionData: failedTransaction }; events.onTransactionStateChange?.("failed", failedTransaction); events.onError?.(tapError); setLastError(tapError); return result2; } }, [currentTransaction, cardql, config.merchantID, connectedReader, events] ); const processPayment = useCallback( async (input) => { if (!connectedReader) { throw new Error("No reader connected"); } try { transactionIdRef.current = generateTransactionId(); const transactionData = { id: transactionIdRef.current, state: "processing_payment", amount: input.amount, currency: input.currency, timestamp: /* @__PURE__ */ new Date() }; setCurrentTransaction(transactionData); events.onTransactionStateChange?.( "processing_payment", transactionData ); if (config.timeout) { timeoutRef.current = setTimeout(() => { handleTimeout(); }, config.timeout); } const paymentIntent = await createPaymentIntent(input); const updatedPaymentIntent = await collectPaymentMethod(paymentIntent); const result2 = await confirmPayment(updatedPaymentIntent); if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = void 0; } return result2; } catch (error) { if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = void 0; } const failedTransaction = { ...currentTransaction, state: "failed" }; setCurrentTransaction(failedTransaction); const tapError = { code: "PAYMENT_COLLECTION_FAILED", message: error.message || "Payment processing failed", details: error, stripeError: error, userFriendlyMessage: "The payment could not be processed", canRetry: true, suggestedAction: "Try the payment again" }; const result2 = { success: false, error: tapError, transactionData: failedTransaction }; events.onTransactionStateChange?.("failed", failedTransaction); events.onError?.(tapError); setLastError(tapError); return result2; } }, [ connectedReader, config.timeout, events, createPaymentIntent, collectPaymentMethod, confirmPayment, currentTransaction ] ); const cancelPayment = useCallback(async () => { try { await stripeTerminalManager.cancelPayment(); if (currentTransaction) { const cancelledTransaction = { ...currentTransaction, state: "cancelled" }; setCurrentTransaction(cancelledTransaction); events.onTransactionStateChange?.("cancelled", cancelledTransaction); } setIsCollectingPayment(false); if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = void 0; } events.onCancel?.(); } catch (error) { setLastError(error); events.onError?.(error); throw error; } }, [currentTransaction, events]); const checkTerminalStatus = useCallback(async () => { return await stripeTerminalManager.getDeviceCapabilities(); }, []); const formatReceipt = useCallback((receiptData) => { return ` RECEIPT ${receiptData.merchantName} ${receiptData.merchantAddress || ""} Transaction ID: ${receiptData.transactionId} ${receiptData.stripePaymentIntentId ? `Stripe ID: ${receiptData.stripePaymentIntentId}` : ""} Amount: ${receiptData.currency} ${receiptData.amount} ${receiptData.tipAmount ? `Tip: ${receiptData.currency} ${receiptData.tipAmount}` : ""} Total: ${receiptData.currency} ${receiptData.totalAmount} ${receiptData.cardType?.toUpperCase()} ****${receiptData.lastFourDigits} ${receiptData.paymentMethod} Auth Code: ${receiptData.authorizationCode} ${receiptData.readerSerialNumber ? `Reader: ${receiptData.readerSerialNumber}` : ""} ${receiptData.timestamp.toLocaleString()} Thank you for your business! `.trim(); }, []); const getConnectionStatus = useCallback(() => { return connectedReader ? "connected" : "notConnected"; }, [connectedReader]); const clearError = useCallback(() => { setLastError(void 0); }, []); const handleTimeout = useCallback(() => { const timeoutError = { code: "TRANSACTION_TIMEOUT", message: "Transaction timed out", userFriendlyMessage: "The transaction timed out. Please try again.", canRetry: true, suggestedAction: "Start a new transaction" }; if (currentTransaction) { const timeoutTransaction = { ...currentTransaction, state: "timeout" }; setCurrentTransaction(timeoutTransaction); events.onTransactionStateChange?.("timeout", timeoutTransaction); } setIsCollectingPayment(false); events.onTimeout?.(); events.onError?.(timeoutError); setLastError(timeoutError); }, [currentTransaction, events]); return { // State isSupported, isInitialized, isDiscoveringReaders, discoveredReaders, connectedReader, isCollectingPayment, currentTransaction, deviceCapabilities, // Actions initialize, discoverReaders, connectReader, disconnectReader, createPaymentIntent, collectPaymentMethod, confirmPayment, processPayment, cancelPayment, // Utilities checkTerminalStatus, formatReceipt, getConnectionStatus, // Error handling lastError, clearError }; } function generateTransactionId() { return `stripe_tap_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } function generateReceiptData(payment, transaction, stripePaymentIntent) { const charge = stripePaymentIntent.charges?.data?.[0]; const paymentMethod = charge?.payment_method_details?.card_present; return { transactionId: transaction.id, stripePaymentIntentId: stripePaymentIntent.id, merchantName: "CardQL Merchant", // This would come from config amount: transaction.amount, currency: transaction.currency, totalAmount: transaction.amount, cardType: paymentMethod?.brand, lastFourDigits: paymentMethod?.last4 || "0000", paymentMethod: "card_present", authorizationCode: charge?.authorization_code, timestamp: transaction.timestamp, readerSerialNumber: charge?.payment_method_details?.card_present?.reader?.serial_number, applicationCryptogram: charge?.payment_method_details?.card_present?.emv_auth_data, applicationPreferredName: paymentMethod?.brand?.toUpperCase(), additionalInfo: { stripe_charge_id: charge?.id, network: paymentMethod?.network, funding: paymentMethod?.funding } }; } // src/components/TapToPayReader.tsx import { useState as useState2, useEffect as useEffect2 } from "react"; import { View, Text, TouchableOpacity, StyleSheet, Animated, Vibration, Alert, FlatList } from "react-native"; import { jsx, jsxs } from "react/jsx-runtime"; function TapToPayReader({ amount, currency = "USD", merchantID, userID, tokenProvider, onSuccess, onError, onCancel, config, readerConfig, style, theme = "light", showAmount = true, showInstructions = true, autoInit = false, autoDiscoverReaders = true, autoConnectReader = false, simulated = false }) { const [animatedValue] = useState2(new Animated.Value(0)); const [isProcessing, setIsProcessing] = useState2(false); const [showReaderSelection, setShowReaderSelection] = useState2(false); const stripeConfig = { tokenProvider, logLevel: "info", simulated }; const tapToPay = useTapToPay({ config: { merchantID, currency, stripeConfig, ...config }, readerConfig: { discoveryMethod: "localMobile", simulated, showAmount, showInstructions, ...readerConfig }, events: { onTransactionStateChange: handleStateChange, onReaderDiscovered: handleReadersDiscovered, onReaderConnected: handleReaderConnected, onReaderDisconnected: handleReaderDisconnected, onPaymentMethodCollected: handlePaymentMethodCollected, onDisplayMessage: handleDisplayMessage, onError: handleError, onSuccess: handleSuccess, onCancel: handleCancel }, autoInit, autoDiscoverReaders, autoConnectReader }); useEffect2(() => { if (tapToPay.isCollectingPayment || isProcessing) { startPulseAnimation(); } else { stopPulseAnimation(); } }, [tapToPay.isCollectingPayment, isProcessing]); const startPulseAnimation = () => { const pulse = () => { Animated.sequence([ Animated.timing(animatedValue, { toValue: 1, duration: 1e3, useNativeDriver: true }), Animated.timing(animatedValue, { toValue: 0, duration: 1e3, useNativeDriver: true }) ]).start(({ finished }) => { if (finished && (tapToPay.isCollectingPayment || isProcessing)) { pulse(); } }); }; pulse(); }; const stopPulseAnimation = () => { animatedValue.stopAnimation(); animatedValue.setValue(0); }; const handleStartPayment = async () => { try { setIsProcessing(true); if (!tapToPay.connectedReader) { if (tapToPay.discoveredReaders.length === 0) { await tapToPay.discoverReaders(); } if (tapToPay.discoveredReaders.length === 1) { await tapToPay.connectReader(tapToPay.discoveredReaders[0]); } else if (tapToPay.discoveredReaders.length > 1) { setShowReaderSelection(true); setIsProcessing(false); return; } else { throw new Error("No readers found"); } } const paymentInput = { amount, currency, merchantID, userID, description: `Stripe Terminal payment - ${amount} ${currency}` }; const result2 = await tapToPay.processPayment(paymentInput); if (result2.success) { onSuccess?.(result2); } else { onError?.(result2.error); } } catch (error) { onError?.(error); } finally { setIsProcessing(false); } }; const handleConnectReader = async (reader) => { try { await tapToPay.connectReader(reader); setShowReaderSelection(false); } catch (error) { onError?.(error); } }; const handleStateChange = (state) => { switch (state) { case "processing_payment_method": if (readerConfig?.vibrationEnabled !== false) { Vibration.vibrate(100); } break; case "processing_payment": case "capturing_payment": setIsProcessing(true); break; case "success": case "failed": case "cancelled": setIsProcessing(false); break; } }; const handleReadersDiscovered = (readers) => { console.log("Readers discovered:", readers.length); }; const handleReaderConnected = (reader) => { console.log("Reader connected:", reader?.id || reader); }; const handleReaderDisconnected = (reason) => { console.log("Reader disconnected:", reason); }; const handlePaymentMethodCollected = (paymentMethod) => { console.log("Payment method collected:", paymentMethod?.type); }; const handleDisplayMessage = (message) => { console.log("Display message:", message); }; const handleError = (error) => { setIsProcessing(false); onError?.(error); Alert.alert("Payment Error", error.userFriendlyMessage || error.message, [ { text: "OK" }, ...error.canRetry ? [{ text: "Try Again", onPress: handleStartPayment }] : [] ]); }; const handleSuccess = (result2) => { onSuccess?.(result2); }; const handleCancel = () => { setIsProcessing(false); onCancel?.(); }; const getStatusText = () => { if (!tapToPay.isSupported) { return "Device not supported for tap-to-pay"; } if (!tapToPay.isInitialized) { return "Initializing payment system..."; } if (tapToPay.isDiscoveringReaders) { return "Searching for card readers..."; } if (!tapToPay.connectedReader && tapToPay.discoveredReaders.length === 0) { return "No card readers found"; } if (!tapToPay.connectedReader && tapToPay.discoveredReaders.length > 0) { return "Select a card reader"; } if (isProcessing) { return "Processing payment..."; } switch (tapToPay.currentTransaction?.state) { case "processing_payment_method": return "Hold your card near the reader"; case "processing_payment": return "Processing payment..."; case "capturing_payment": return "Finalizing payment..."; case "success": return readerConfig?.successMessage || "Payment successful"; case "failed": return readerConfig?.errorMessage || "Payment failed"; case "cancelled": return "Payment cancelled"; case "timeout": return "Payment timed out"; default: return tapToPay.connectedReader ? "Tap to start payment" : "Connect a card reader"; } }; const getStatusColor = () => { if (!tapToPay.isSupported || !tapToPay.isInitialized) { return "#FF6B6B"; } if (tapToPay.isDiscoveringReaders) { return "#FFA726"; } switch (tapToPay.currentTransaction?.state) { case "processing_payment_method": return "#4ECDC4"; case "processing_payment": case "capturing_payment": return "#45B7D1"; case "success": return "#66BB6A"; case "failed": case "timeout": return "#FF6B6B"; case "cancelled": return "#BDBDBD"; default: return theme === "dark" ? "#FFFFFF" : "#000000"; } }; const canStartPayment = tapToPay.isSupported && tapToPay.isInitialized && tapToPay.connectedReader && !tapToPay.isCollectingPayment && !isProcessing; const canDiscoverReaders = tapToPay.isSupported && tapToPay.isInitialized && !tapToPay.isDiscoveringReaders; const canCancelPayment = tapToPay.isCollectingPayment || isProcessing; if (showReaderSelection) { return /* @__PURE__ */ jsxs( View, { style: [ styles.container, theme === "dark" && styles.darkContainer, style ], children: [ /* @__PURE__ */ jsx(Text, { style: [styles.title, theme === "dark" && styles.darkText], children: "Select Card Reader" }), /* @__PURE__ */ jsx( FlatList, { data: tapToPay.discoveredReaders, keyExtractor: (item) => item.id || item.serialNumber, renderItem: ({ item }) => /* @__PURE__ */ jsxs( TouchableOpacity, { style: styles.readerItem, onPress: () => handleConnectReader(item), children: [ /* @__PURE__ */ jsx( Text, { style: [ styles.readerLabel, theme === "dark" && styles.darkText ], children: item.label || item.id } ), /* @__PURE__ */ jsx( Text, { style: [ styles.readerType, theme === "dark" && styles.darkText ], children: item.deviceType } ) ] } ) } ), /* @__PURE__ */ jsx( TouchableOpacity, { style: [styles.button, styles.cancelButton], onPress: () => setShowReaderSelection(false), children: /* @__PURE__ */ jsx(Text, { style: styles.buttonText, children: "Cancel" }) } ) ] } ); } return /* @__PURE__ */ jsxs( View, { style: [ styles.container, theme === "dark" && styles.darkContainer, style ], children: [ showAmount && /* @__PURE__ */ jsxs(View, { style: styles.amountContainer, children: [ /* @__PURE__ */ jsx(Text, { style: [styles.currency, theme === "dark" && styles.darkText], children: currency }), /* @__PURE__ */ jsx(Text, { style: [styles.amount, theme === "dark" && styles.darkText], children: amount }) ] }), /* @__PURE__ */ jsxs(View, { style: styles.readerContainer, children: [ /* @__PURE__ */ jsx( Animated.View, { style: [ styles.terminalIcon, { opacity: animatedValue.interpolate({ inputRange: [0, 1], outputRange: [0.3, 1] }), transform: [ { scale: animatedValue.interpolate({ inputRange: [0, 1], outputRange: [1, 1.1] }) } ] } ], children: /* @__PURE__ */ jsx( View, { style: [styles.terminalSymbol, { borderColor: getStatusColor() }], children: /* @__PURE__ */ jsx(Text, { style: [styles.terminalText, { color: getStatusColor() }], children: "\u{1F4B3}" }) } ) } ), /* @__PURE__ */ jsx( Text, { style: [ styles.statusText, { color: getStatusColor() }, theme === "dark" && styles.darkText ], children: getStatusText() } ), tapToPay.connectedReader && /* @__PURE__ */ jsxs( Text, { style: [styles.readerInfo, theme === "dark" && styles.darkText], children: [ "Reader:", " ", tapToPay.connectedReader.label || tapToPay.connectedReader.id ] } ), tapToPay.currentTransaction?.stripePaymentMethod && /* @__PURE__ */ jsxs(Text, { style: [styles.cardInfo, theme === "dark" && styles.darkText], children: [ tapToPay.currentTransaction.cardType?.toUpperCase(), tapToPay.currentTransaction.lastFourDigits && ` ****${tapToPay.currentTransaction.lastFourDigits}` ] }) ] }), /* @__PURE__ */ jsxs(View, { style: styles.buttonContainer, children: [ canStartPayment && /* @__PURE__ */ jsx( TouchableOpacity, { style: [styles.button, styles.startButton], onPress: handleStartPayment, children: /* @__PURE__ */ jsx(Text, { style: styles.buttonText, children: "Start Payment" }) } ), !tapToPay.connectedReader && canDiscoverReaders && /* @__PURE__ */ jsx( TouchableOpacity, { style: [styles.button, styles.discoverButton], onPress: () => tapToPay.discoverReaders(), children: /* @__PURE__ */ jsx(Text, { style: styles.buttonText, children: tapToPay.discoveredReaders.length > 0 ? "Find More Readers" : "Find Readers" }) } ), canCancelPayment && /* @__PURE__ */ jsx( TouchableOpacity, { style: [styles.button, styles.cancelButton], onPress: () => tapToPay.cancelPayment(), children: /* @__PURE__ */ jsx(Text, { style: styles.buttonText, children: "Cancel" }) } ) ] }), tapToPay.lastError && /* @__PURE__ */ jsxs(View, { style: styles.errorContainer, children: [ /* @__PURE__ */ jsx(Text, { style: styles.errorText, children: tapToPay.lastError.userFriendlyMessage || tapToPay.lastError.message }), tapToPay.lastError.canRetry && /* @__PURE__ */ jsx( TouchableOpacity, { style: [styles.button, styles.retryButton], onPress: () => { tapToPay.clearError(); handleStartPayment(); }, children: /* @__PURE__ */ jsx(Text, { style: styles.buttonText, children: "Try Again" }) } ) ] }), /* @__PURE__ */ jsx(View, { style: styles.statusContainer, children: /* @__PURE__ */ jsxs( Text, { style: [styles.deviceStatus, theme === "dark" && styles.darkText], children: [ "Terminal:", " ", tapToPay.isSupported ? tapToPay.isInitialized ? tapToPay.connectedReader ? "Connected" : "Ready" : "Initializing" : "Not Supported" ] } ) }) ] } ); } var styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#F8F9FA", padding: 20, alignItems: "center", justifyContent: "center" }, darkContainer: { backgroundColor: "#1E1E1E" }, title: { fontSize: 24, fontWeight: "bold", marginBottom: 20, color: "#000" }, amountContainer: { alignItems: "center", marginBottom: 40 }, currency: { fontSize: 16, color: "#666", fontWeight: "500" }, amount: { fontSize: 48, fontWeight: "bold", color: "#000" }, readerContainer: { alignItems: "center", marginVertical: 40 }, terminalIcon: { marginBottom: 20 }, terminalSymbol: { width: 120, height: 120, borderRadius: 60, borderWidth: 3, alignItems: "center", justifyContent: "center", backgroundColor: "rgba(78, 205, 196, 0.1)" }, terminalText: { fontSize: 32, fontWeight: "bold" }, statusText: { fontSize: 18, textAlign: "center", marginBottom: 10, fontWeight: "500" }, readerInfo: { fontSize: 14, color: "#666", textAlign: "center", marginBottom: 5 }, cardInfo: { fontSize: 14, color: "#666", textAlign: "center" }, buttonContainer: { flexDirection: "row", flexWrap: "wrap", justifyContent: "center", marginTop: 20, gap: 15 }, button: { paddingHorizontal: 24, paddingVertical: 12, borderRadius: 8, minWidth: 120, alignItems: "center" }, startButton: { backgroundColor: "#4ECDC4" }, discoverButton: { backgroundColor: "#45B7D1" }, cancelButton: { backgroundColor: "#FF6B6B" }, retryButton: { backgroundColor: "#FFA726", marginTop: 10 }, buttonText: { color: "#FFFFFF", fontSize: 16, fontWeight: "600" }, readerItem: { padding: 15, backgroundColor: "#FFFFFF", borderRadius: 8, marginBottom: 10, borderWidth: 1, borderColor: "#E0E0E0" }, readerLabel: { fontSize: 16, fontWeight: "600", color: "#000" }, readerType: { fontSize: 14, color: "#666", marginTop: 5 }, errorContainer: { marginTop: 20, padding: 15, backgroundColor: "#FFEBEE", borderRadius: 8, alignItems: "center", maxWidth: "100%" }, errorText: { color: "#C62828", fontSize: 14, textAlign: "center", marginBottom: 10 }, statusContainer: { position: "absolute", bottom: 20, alignItems: "center" }, deviceStatus: { fontSize: 12, color: "#666" }, darkText: { color: "#FFFFFF" } }); export { StripeTerminalManager, TapToPayReader, stripeTerminalManager, useTapToPay };