Skip to content

Instantly share code, notes, and snippets.

@aayushchugh
Created August 24, 2025 17:58
Show Gist options
  • Select an option

  • Save aayushchugh/c962710ad6a1a422c8b2fa7584c01d38 to your computer and use it in GitHub Desktop.

Select an option

Save aayushchugh/c962710ad6a1a422c8b2fa7584c01d38 to your computer and use it in GitHub Desktop.
Tap to pay in stripe
import {
useStripeTerminal,
type PaymentIntent,
type StripeError,
CommonError
} from '@stripe/stripe-terminal-react-native';
import { showError, showSuccess, showInfo } from '@/utils/toast';
import { useRouter } from 'expo-router';
import axios from 'axios';
import url from '@/constants/url';
import { Alert } from 'react-native';
import { getFromKeychain, getFromKeychainWithRefresh, KEYCHAIN_KEYS } from '@/utils/keychain';
import { useAuth } from '@/context/AuthContext';
interface PaymentData {
amount: number; // Amount in cents
currency: string;
quantity: number;
ticket: any;
firstName: string;
lastName: string;
customerEmail: string;
customerPhone: string;
eventId: string;
ticketId: string;
userId: string;
organizerId: string;
eventName: string;
ticketPrice: number;
ticketName: string;
autoCheckin?: boolean;
}
interface PaymentResult {
success: boolean;
paymentIntent?: any;
user_id?: string;
intentId?: string;
error?: StripeError;
}
export const useTapToPayPaymentHandler = () => {
const router = useRouter();
const { organizerDetails } = useAuth();
const {
createPaymentIntent,
collectPaymentMethod,
confirmPaymentIntent,
retrievePaymentIntent,
connectedReader,
isInitialized,
} = useStripeTerminal();
const processTapToPayPayment = async (paymentData: PaymentData): Promise<PaymentResult> => {
// Check if organizer has Stripe account access
if (!organizerDetails?.stripeAccountId) {
const error: StripeError = {
code: CommonError.Failed,
message: 'This organization does not have payment processing capabilities. Please contact the organization owner.',
};
return { success: false, error };
}
// Check if reader is connected
if (!connectedReader) {
const error: StripeError = {
code: CommonError.Failed,
message: 'Tap to Pay reader is not connected. Please connect a reader first.',
};
return { success: false, error };
}
// Check if terminal is initialized
if (!isInitialized) {
const error: StripeError = {
code: CommonError.Failed,
message: 'Stripe Terminal is not initialized.',
};
return { success: false, error };
}
const accessToken = await getFromKeychainWithRefresh(KEYCHAIN_KEYS.AUTH_TOKEN);
if (!accessToken) {
throw new Error("No access token found");
}
try {
const firstName = paymentData.firstName || '';
const lastName = paymentData.lastName || '';
const requestPayload = {
eventId: paymentData.eventId,
firstName,
lastName,
...(paymentData.customerEmail && { email: paymentData.customerEmail }),
...(paymentData.customerPhone && { phone: "+1" + paymentData.customerPhone }),
paymentMethod: "tap_to_pay",
autoCheckin: paymentData.autoCheckin ?? true, // Default to true if not provided
tickets: [
{
ticketId: paymentData.ticketId,
quantity: paymentData.quantity,
price: paymentData.ticketPrice * 100,
}
]
};
// Step 1: Create Payment Intent
let clientSecret: string;
let paymentId: string;
try {
const response = await axios.post(`${url}/v1/organizer/${paymentData.organizerId}/events/walkin-ticket`, requestPayload, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
const paymentIntentData = response.data.payload;
clientSecret = paymentIntentData.clientSecret;
paymentId = paymentIntentData.intentId || paymentIntentData.payment_id || paymentIntentData.id; // Extract intentId from backend response
if (!clientSecret) {
showError('Could not prepare payment. Try again.');
return { success: false, error: { code: CommonError.Failed, message: 'Missing payment intent data' } };
}
} catch (apiError: any) {
if (apiError.response?.status === 400) {
const errorMessage = apiError.response?.data?.message || 'Invalid payment details. Please check your information.';
showError(errorMessage);
} else if (apiError.response?.status === 500) {
const errorMessage = apiError.response?.data?.message || 'Server error. Please try again later.';
showError(errorMessage);
} else if (apiError.code === 'ERR_NETWORK') {
showError('Network error. Please check your connection and try again.');
} else {
showError('Payment setup failed. Please try again.');
}
return { success: false, error: { code: CommonError.Failed, message: 'API request failed' } };
}
const { error: retrieveError, paymentIntent } = await retrievePaymentIntent(clientSecret);
if (retrieveError) {
// Show user-friendly error message for device-busy scenarios
if (shouldShowPaymentError(retrieveError)) {
const errorMessage = getPaymentErrorMessage(retrieveError);
showError(errorMessage);
}
return { success: false, error: retrieveError };
}
if (!paymentIntent) {
const error: StripeError = {
code: CommonError.Failed,
message: 'Unable to retrieve payment information. Please try again.',
};
showError('Unable to retrieve payment information. Please try again.');
return { success: false, error };
}
// Step 2: Collect Payment Method
const { paymentIntent: collectedIntent, error: collectError } = await collectPaymentMethod({
paymentIntent: paymentIntent,
skipTipping: true,
updatePaymentIntent: true,
enableCustomerCancellation: true,
requestDynamicCurrencyConversion: false,
allowRedisplay: 'limited', // Required for Terminal payment method collection
});
if (collectError) {
// Show user-friendly error message for device-busy scenarios and network issues
if (shouldShowPaymentError(collectError)) {
const errorMessage = getPaymentErrorMessage(collectError);
showError(errorMessage);
}
// Check if user canceled the payment
if (collectError.code.toString() === 'Canceled' || collectError.code.toString() === 'canceled') {
// Cancel the payment intent on the backend
if (paymentIntent?.id) {
await cancelPaymentIntentOnBackend(paymentIntent.id);
}
return { success: false, error: collectError };
}
return { success: false, error: collectError };
}
if (!collectedIntent) {
return { success: false, error: { code: CommonError.Failed, message: 'No payment intent collected' } };
}
// Step 3: Confirm Payment Intent
const { paymentIntent: confirmedIntent, error: confirmError } = await confirmPaymentIntent({
paymentIntent: collectedIntent,
});
if (confirmError) {
// Show user-friendly error message for device-busy scenarios and network issues
if (shouldShowPaymentError(confirmError)) {
const errorMessage = getPaymentErrorMessage(confirmError);
showError(errorMessage);
}
return { success: false, error: confirmError };
}
if (!confirmedIntent) {
const error: StripeError = {
code: CommonError.Failed,
message: 'Payment confirmation failed. Please try again.',
};
showError('Payment confirmation failed. Please try again.');
return { success: false, error };
}
return {
success: true,
paymentIntent: confirmedIntent,
intentId: paymentId,
};
} catch (error) {
const unexpectedError: StripeError = {
code: CommonError.Failed,
message: 'An unexpected error occurred during payment processing.',
};
return { success: false, error: unexpectedError };
}
};
const cancelPayment = async (): Promise<void> => {
try {
// Cancel any ongoing payment operations
// This would typically involve canceling the current operation
} catch (error) {
}
};
const cancelPaymentIntentOnBackend = async (paymentIntentId: string) => {
try {
const response = await fetch(`${url}/stripe-terminal/cancel-payment-intent`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ paymentIntentId }),
});
if (!response.ok) {
throw new Error('Failed to cancel PaymentIntent');
}
const result = await response.json();
} catch (error) {
}
};
return {
processTapToPayPayment,
cancelPayment,
connectedReader,
isInitialized,
};
};
// Helper function to format payment data
export const formatPaymentData = (params: any): PaymentData => {
return {
amount: Number(params.amount) || 0,
currency: 'usd',
quantity: Number(params.quantity) || 1,
ticket: params.ticket ? JSON.parse(params.ticket as string) : null,
firstName: params.firstName as string || '',
lastName: params.lastName as string || '',
customerEmail: params.customerEmail as string || '',
customerPhone: params.customerPhone as string || '',
eventId: params.eventId as string || '',
ticketId: params.ticketId as string || '',
userId: params.userId as string || '',
organizerId: params.organizerId as string || '',
eventName: params.eventName as string || '',
ticketPrice: Number(params.ticketPrice) || 0,
ticketName: params.ticketName as string || '',
autoCheckin: params.autoCheckin !== undefined ? Boolean(params.autoCheckin) : true,
};
};
// Helper function to get user-friendly error message for payment processing
function getPaymentErrorMessage(error: StripeError): string {
const errorCode = error.code.toString();
switch (errorCode) {
case 'CommandNotAllowedDuringCall':
case '2930':
return 'Payment cannot be processed while a phone call is active. Please end your call and try again.';
case 'UnsupportedMobileDeviceConfiguration':
case '2910':
return 'Your device configuration is not supported for payments. Please check your device settings and try again.';
case 'PasscodeNotEnabled':
case '2920':
return 'Payment processing requires a device passcode. Please set up a passcode in your device settings and try again.';
case 'CommandNotAllowed':
case '2900':
return 'Payment processing is not available on this device. Please check your device capabilities and try again.';
case 'ReaderBusy':
case '3010':
return 'The card reader is busy. Please wait a moment and try again.';
case 'Canceled':
case 'canceled':
return 'Payment was cancelled.';
default:
return error.message || 'An unexpected error occurred during payment processing. Please try again.';
}
}
// Helper function to check if error should show user-friendly message
function shouldShowPaymentError(error: StripeError): boolean {
const errorCode = error.code.toString();
// Show user-friendly messages for device-busy scenarios and network issues
if (errorCode === 'CommandNotAllowedDuringCall' || errorCode === '2930') {
return true;
}
if (errorCode === 'UnsupportedMobileDeviceConfiguration' || errorCode === '2910') {
return true;
}
if (errorCode === 'PasscodeNotEnabled' || errorCode === '2920') {
return true;
}
if (errorCode === 'CommandNotAllowed' || errorCode === '2900') {
return true;
}
if (errorCode === 'ReaderBusy' || errorCode === '3010') {
return true;
}
// Don't show message for user cancellation
if (errorCode === 'Canceled' || errorCode === 'canceled') {
return false;
}
// Don't show message for payment failures (declined cards, insufficient funds, etc.)
// These should be handled by the UI navigation to step-4
if (errorCode === 'Failed' || errorCode === 'Unknown') {
return false;
}
// Show message for other errors (network issues, etc.)
return true;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment