UNPKG

@cardql/react-native-tap

Version:

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

497 lines (432 loc) 14.8 kB
import { Platform } from "react-native"; import type { NFCCapabilities, DeviceCapabilities, CardType, PaymentMethod, TapToPayError, TapToPayErrorCode, StripeTerminalConfig, StripeReaderConfig, TapTransactionData, } from "../types"; // Import Stripe Terminal SDK with fallback for development let StripeTerminal: any; let useStripeTerminal: any; 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"); // Mock implementation for development 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, }); } export class StripeTerminalManager { private isInitialized = false; private config?: StripeTerminalConfig; private connectedReader?: any; private discoveredReaders: any[] = []; private currentPaymentIntent?: any; async initialize(config: StripeTerminalConfig): Promise<void> { if (this.isInitialized) return; try { this.config = config; // Initialize Stripe Terminal with token provider await StripeTerminal.initialize({ tokenProvider: config.tokenProvider, logLevel: config.logLevel || "info", }); this.isInitialized = true; } catch (error: any) { throw this.createError( "TERMINAL_NOT_INITIALIZED", `Failed to initialize Stripe Terminal: ${error.message}`, error ); } } async checkCapabilities(): Promise<NFCCapabilities> { 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(): Promise<DeviceCapabilities> { 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: StripeReaderConfig): Promise<any[]> { 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: any) { if (error.code) throw error; throw this.createError( "READER_DISCOVERY_FAILED", `Failed to discover readers: ${error.message}`, error ); } } async connectReader(reader: any, config?: StripeReaderConfig): Promise<any> { if (!this.isInitialized) { throw this.createError( "TERMINAL_NOT_INITIALIZED", "Terminal not initialized" ); } try { let result; if (config?.discoveryMethod === "localMobile") { result = await StripeTerminal.connectLocalMobileReader({ reader, locationId: config?.locationId, }); } else { result = await StripeTerminal.connectBluetoothReader({ reader, locationId: config?.locationId, }); } if (result.error) { throw this.createError( "READER_CONNECTION_FAILED", `Reader connection failed: ${result.error.message}`, result.error ); } this.connectedReader = result.reader; return result.reader; } catch (error: any) { if (error.code) throw error; throw this.createError( "READER_CONNECTION_FAILED", `Failed to connect reader: ${error.message}`, error ); } } async disconnectReader(): Promise<void> { if (!this.connectedReader) return; try { await StripeTerminal.disconnectReader(); this.connectedReader = undefined; } catch (error: any) { throw this.createError( "READER_CONNECTION_FAILED", `Failed to disconnect reader: ${error.message}`, error ); } } async createPaymentIntent( amount: number, currency: string, metadata?: any ): Promise<any> { 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: any) { if (error.code) throw error; throw this.createError( "PAYMENT_INTENT_CREATION_FAILED", `Failed to create payment intent: ${error.message}`, error ); } } async collectPaymentMethod(paymentIntent: any): Promise<any> { 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: any) { if (error.code) throw error; throw this.createError( "PAYMENT_COLLECTION_FAILED", `Failed to collect payment method: ${error.message}`, error ); } } async confirmPaymentIntent(paymentIntent: any): Promise<any> { 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: any) { if (error.code) throw error; throw this.createError( "PAYMENT_CONFIRMATION_FAILED", `Failed to confirm payment intent: ${error.message}`, error ); } } async cancelPayment(): Promise<void> { try { await StripeTerminal.cancelCollectPaymentMethod(); } catch (error: any) { throw this.createError( "PAYMENT_COLLECTION_FAILED", `Failed to cancel payment: ${error.message}`, error ); } } getConnectedReader(): any { return this.connectedReader; } getDiscoveredReaders(): any[] { return this.discoveredReaders; } getCurrentPaymentIntent(): any { return this.currentPaymentIntent; } // Private helper methods private async isTerminalSupported(): Promise<boolean> { try { // Check if device supports Stripe Terminal return Platform.OS === "ios" || Platform.OS === "android"; } catch { return false; } } private getSupportedCardTypes(): CardType[] { // Stripe Terminal supports most major card types return [ "visa", "mastercard", "amex", "discover", "diners", "jcb", "unionpay", "contactless", ]; } private getMaxTransactionAmount(): string { // This would be configured based on merchant settings return "999999.99"; } private hasSecureElement(): boolean { // Stripe Terminal handles secure element requirements return Platform.OS === "ios" || Platform.OS === "android"; } private getDeviceModel(): string { return Platform.OS === "ios" ? "iPhone" : "Android Device"; } private getStripeTerminalVersion(): string { // This would come from the SDK version return "0.0.1-beta.25"; } private detectCardType(paymentMethod: any): CardType { if (!paymentMethod?.card?.brand) return "unknown"; const brand = paymentMethod.card.brand.toLowerCase(); const cardTypeMap: Record<string, CardType> = { visa: "visa", mastercard: "mastercard", amex: "amex", discover: "discover", diners: "diners", jcb: "jcb", unionpay: "unionpay", }; return cardTypeMap[brand] || "unknown"; } private detectPaymentMethod(paymentMethod: any): PaymentMethod { if (!paymentMethod?.type) return "unknown"; switch (paymentMethod.type) { case "card_present": return "card_present"; default: return "contactless_card"; } } private getLastFourDigits(paymentMethod: any): string { return paymentMethod?.card?.last4 || "0000"; } private generateTransactionId(): string { return `stripe_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } private createError( code: TapToPayErrorCode, message: string, details?: any ): TapToPayError { return { code, message, details, stripeError: details, userFriendlyMessage: this.getUserFriendlyMessage(code), canRetry: this.canRetryError(code), suggestedAction: this.getSuggestedAction(code), }; } private getUserFriendlyMessage(code: TapToPayErrorCode): string { const messages: Record<TapToPayErrorCode, string> = { 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"; } private canRetryError(code: TapToPayErrorCode): boolean { const retryableErrors: TapToPayErrorCode[] = [ "READER_CONNECTION_FAILED", "READER_DISCOVERY_FAILED", "PAYMENT_COLLECTION_FAILED", "CARD_READ_ERROR", "TRANSACTION_TIMEOUT", "NETWORK_ERROR", "STRIPE_ERROR", "UNKNOWN_ERROR", ]; return retryableErrors.includes(code); } private getSuggestedAction(code: TapToPayErrorCode): string { const actions: Record<TapToPayErrorCode, string> = { 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"; } } export const stripeTerminalManager = new StripeTerminalManager();