@cardql/react-native-tap
Version:
CardQL SDK for React Native tap-to-pay for secure in-person payments
1,479 lines (1,470 loc) • 51.5 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
StripeTerminalManager: () => StripeTerminalManager,
TapToPayReader: () => TapToPayReader,
stripeTerminalManager: () => stripeTerminalManager,
useTapToPay: () => useTapToPay
});
module.exports = __toCommonJS(index_exports);
__reExport(index_exports, require("@cardql/core"), module.exports);
__reExport(index_exports, require("@cardql/react-native"), module.exports);
// src/hooks/useTapToPay.ts
var import_react = require("react");
var import_react_native2 = require("@cardql/react-native");
// src/utils/nfcManager.ts
var import_react_native = require("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: import_react_native.Platform.OS === "ios" || import_react_native.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: import_react_native.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 import_react_native.Platform.OS === "ios" || import_react_native.Platform.OS === "android";
} catch {
return false;
}
}
getSupportedCardTypes() {
return [
"visa",
"mastercard",
"amex",
"discover",
"diners",
"jcb",
"unionpay",
"contactless"
];
}
getMaxTransactionAmount() {
return "999999.99";
}
hasSecureElement() {
return import_react_native.Platform.OS === "ios" || import_react_native.Platform.OS === "android";
}
getDeviceModel() {
return import_react_native.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 = (0, import_react_native2.useCardQLClient)();
const [isSupported, setIsSupported] = (0, import_react.useState)(false);
const [isInitialized, setIsInitialized] = (0, import_react.useState)(false);
const [isDiscoveringReaders, setIsDiscoveringReaders] = (0, import_react.useState)(false);
const [discoveredReaders, setDiscoveredReaders] = (0, import_react.useState)([]);
const [connectedReader, setConnectedReader] = (0, import_react.useState)();
const [isCollectingPayment, setIsCollectingPayment] = (0, import_react.useState)(false);
const [currentTransaction, setCurrentTransaction] = (0, import_react.useState)();
const [deviceCapabilities, setDeviceCapabilities] = (0, import_react.useState)();
const [lastError, setLastError] = (0, import_react.useState)();
const timeoutRef = (0, import_react.useRef)();
const transactionIdRef = (0, import_react.useRef)();
const currentPaymentIntentRef = (0, import_react.useRef)();
(0, import_react.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]);
(0, import_react.useEffect)(() => {
if (autoDiscoverReaders && isInitialized && !isDiscoveringReaders && discoveredReaders.length === 0) {
discoverReaders();
}
}, [
autoDiscoverReaders,
isInitialized,
isDiscoveringReaders,
discoveredReaders.length
]);
(0, import_react.useEffect)(() => {
if (autoConnectReader && discoveredReaders.length > 0 && !connectedReader) {
connectReader(discoveredReaders[0]);
}
}, [autoConnectReader, discoveredReaders, connectedReader]);
(0, import_react.useEffect)(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
cancelPayment();
};
}, []);
const initialize = (0, import_react.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 = (0, import_react.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 = (0, import_react.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 = (0, import_react.useCallback)(async () => {
try {
await stripeTerminalManager.disconnectReader();
setConnectedReader(void 0);
events.onReaderDisconnected?.();
} catch (error) {
setLastError(error);
events.onError?.(error);
throw error;
}
}, [events]);
const createPaymentIntent = (0, import_react.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 = (0, import_react.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 = (0, import_react.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 = (0, import_react.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 = (0, import_react.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 = (0, import_react.useCallback)(async () => {
return await stripeTerminalManager.getDeviceCapabilities();
}, []);
const formatReceipt = (0, import_react.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 = (0, import_react.useCallback)(() => {
return connectedReader ? "connected" : "notConnected";
}, [connectedReader]);
const clearError = (0, import_react.useCallback)(() => {
setLastError(void 0);
}, []);
const handleTimeout = (0, import_react.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
var import_react2 = require("react");
var import_react_native3 = require("react-native");
var import_jsx_runtime = require("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] = (0, import_react2.useState)(new import_react_native3.Animated.Value(0));
const [isProcessing, setIsProcessing] = (0, import_react2.useState)(false);
const [showReaderSelection, setShowReaderSelection] = (0, import_react2.useState)(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
});
(0, import_react2.useEffect)(() => {
if (tapToPay.isCollectingPayment || isProcessing) {
startPulseAnimation();
} else {
stopPulseAnimation();
}
}, [tapToPay.isCollectingPayment, isProcessing]);
const startPulseAnimation = () => {
const pulse = () => {
import_react_native3.Animated.sequence([
import_react_native3.Animated.timing(animatedValue, {
toValue: 1,
duration: 1e3,
useNativeDriver: true
}),
import_react_native3.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) {
import_react_native3.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);
import_react_native3.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__ */ (0, import_jsx_runtime.jsxs)(
import_react_native3.View,
{
style: [
styles.container,
theme === "dark" && styles.darkContainer,
style
],
children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native3.Text, { style: [styles.title, theme === "dark" && styles.darkText], children: "Select Card Reader" }),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_react_native3.FlatList,
{
data: tapToPay.discoveredReaders,
keyExtractor: (item) => item.id || item.serialNumber,
renderItem: ({ item }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
import_react_native3.TouchableOpacity,
{
style: styles.readerItem,
onPress: () => handleConnectReader(item),
children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_react_native3.Text,
{
style: [
styles.readerLabel,
theme === "dark" && styles.darkText
],
children: item.label || item.id
}
),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_react_native3.Text,
{
style: [
styles.readerType,
theme === "dark" && styles.darkText
],
children: item.deviceType
}
)
]
}
)
}
),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_react_native3.TouchableOpacity,
{
style: [styles.button, styles.cancelButton],
onPress: () => setShowReaderSelection(false),
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native3.Text, { style: styles.buttonText, children: "Cancel" })
}
)
]
}
);
}
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
import_react_native3.View,
{
style: [
styles.container,
theme === "dark" && styles.darkContainer,
style
],
children: [
showAmount && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native3.View, { style: styles.amountContainer, children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native3.Text, { style: [styles.currency, theme === "dark" && styles.darkText], children: currency }),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native3.Text, { style: [styles.amount, theme === "dark" && styles.darkText], children: amount })
] }),
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native3.View, { style: styles.readerContainer, children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_react_native3.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__ */ (0, import_jsx_runtime.jsx)(
import_react_native3.View,
{
style: [styles.terminalSymbol, { borderColor: getStatusColor() }],
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native3.Text, { style: [styles.terminalText, { color: getStatusColor() }], children: "\u{1F4B3}" })
}
)
}
),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_react_native3.Text,
{
style: [
styles.statusText,
{ color: getStatusColor() },
theme === "dark" && styles.darkText
],
children: getStatusText()
}
),
tapToPay.connectedReader && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
import_react_native3.Text,
{
style: [styles.readerInfo, theme === "dark" && styles.darkText],
children: [
"Reader:",
" ",
tapToPay.connectedReader.label || tapToPay.connectedReader.id
]
}
),
tapToPay.currentTransaction?.stripePaymentMethod && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native3.Text, { style: [styles.cardInfo, theme === "dark" && styles.darkText], children: [
tapToPay.currentTransaction.cardType?.toUpperCase(),
tapToPay.currentTransaction.lastFourDigits && ` ****${tapToPay.currentTransaction.lastFourDigits}`
] })
] }),
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native3.View, { style: styles.buttonContainer, children: [
canStartPayment && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_react_native3.TouchableOpacity,
{
style: [styles.button, styles.startButton],
onPress: handleStartPayment,
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native3.Text, { style: styles.buttonText, children: "Start Payment" })
}
),
!tapToPay.connectedReader && canDiscoverReaders && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_react_native3.TouchableOpacity,
{
style: [styles.button, styles.discoverButton],
onPress: () => tapToPay.discoverReaders(),
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native3.Text, { style: styles.buttonText, children: tapToPay.discoveredReaders.length > 0 ? "Find More Readers" : "Find Readers" })
}
),
canCancelPayment && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_react_native3.TouchableOpacity,
{
style: [styles.button, styles.cancelButton],
onPress: () => tapToPay.cancelPayment(),
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native3.Text, { style: styles.buttonText, children: "Cancel" })
}
)
] }),
tapToPay.lastError && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native3.View, { style: styles.errorContainer, children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native3.Text, { style: styles.errorText, children: tapToPay.lastError.userFriendlyMessage || tapToPay.lastError.message }),
tapToPay.lastError.canRetry && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_react_native3.TouchableOpacity,
{
style: [styles.button, styles.retryButton],
onPress: () => {
tapToPay.clearError();
handleStartPayment();
},
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native3.Text, { style: styles.buttonText, children: "Try Again" })
}
)
] }),
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native3.View, { style: styles.statusContainer, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
import_react_native3.Text,
{
style: [styles.deviceStatus, theme === "dark" && styles.darkText],
children: [
"Terminal:",
" ",
tapToPay.isSupported ? tapToPay.isInitialized ? tapToPay.connectedReader ? "Connected" : "Ready" : "Initializing" : "Not Supported"
]
}
) })
]
}
);
}
var styles = import_react_native3.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: "