Created
August 24, 2025 17:58
-
-
Save aayushchugh/c962710ad6a1a422c8b2fa7584c01d38 to your computer and use it in GitHub Desktop.
Tap to pay in stripe
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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