From 022fa69c0ac77c38929d6a9f9e0e62a547a5f22a Mon Sep 17 00:00:00 2001 From: Tamojit Biswas Date: Fri, 17 Jul 2026 14:16:10 +0530 Subject: [PATCH] feat: implement payment processing with Stripe and add documentation for tracking and payment integration. --- app/App.tsx | 10 +- app/api/paymentMethodsApi.ts | 18 ++++ app/config/index.ts | 10 ++ .../checkoutPaymentScreen.tsx | 64 ++++++----- .../hooks/useStripePayment.ts | 102 ++++++++++++++++++ app/interfaces/order.ts | 10 ++ 6 files changed, 187 insertions(+), 27 deletions(-) create mode 100644 app/config/index.ts create mode 100644 app/features/screens/checkoutPaymentScreen/hooks/useStripePayment.ts diff --git a/app/App.tsx b/app/App.tsx index d774074..0bf5022 100644 --- a/app/App.tsx +++ b/app/App.tsx @@ -11,14 +11,18 @@ import { persistor, store } from './store'; import { RootNavigator } from './navigation/rootNavigator'; import { PersistGate } from 'redux-persist/integration/react'; import { colors } from '@theme'; +import { StripeProvider } from '@stripe/stripe-react-native'; +import { ENV } from './config'; function App() { return ( - - - + + + + + ); diff --git a/app/api/paymentMethodsApi.ts b/app/api/paymentMethodsApi.ts index e27ef59..148be13 100644 --- a/app/api/paymentMethodsApi.ts +++ b/app/api/paymentMethodsApi.ts @@ -4,3 +4,21 @@ import { apiClient } from '@services'; export const getAllPaymentMethodsApi = async () => { return await apiClient.get('/payments/options'); }; + +export interface VerifyPaymentPayload { + gatewayOrderId: string; + gatewayPaymentId: string; + gatewaySignature: string; +} + +export interface VerifyPaymentResponse { + success: boolean; + message: string; +} + +export const verifyPaymentApi = async (payload: VerifyPaymentPayload) => { + return await apiClient.post( + '/payments/process', + payload, + ); +}; diff --git a/app/config/index.ts b/app/config/index.ts new file mode 100644 index 0000000..478c2dd --- /dev/null +++ b/app/config/index.ts @@ -0,0 +1,10 @@ +// ─── Environment Configuration ─────────────────────────────────────────────── +// Centralizes all environment keys for the application. +// Since React Native doesn't natively expose process.env at runtime, +// we export them directly from here. + +export const ENV = { + STRIPE_PUBLISHABLE_KEY: + 'pk_test_51Ox04DSHlFQYe8R5Cvm8i6n99QynUNj7WQzACB89PImnt0X8Z54SBFM22ghSNHYFo7OgVcyba9QhhyrrdRqRPJpF00Us8XJv7f', + RAZORPAY_KEY_ID: 'rzp_test_TCxbW8AxcXgMCj', +}; diff --git a/app/features/screens/checkoutPaymentScreen/checkoutPaymentScreen.tsx b/app/features/screens/checkoutPaymentScreen/checkoutPaymentScreen.tsx index 1ca579d..9468b03 100644 --- a/app/features/screens/checkoutPaymentScreen/checkoutPaymentScreen.tsx +++ b/app/features/screens/checkoutPaymentScreen/checkoutPaymentScreen.tsx @@ -16,6 +16,7 @@ import { getAllPaymentMethodsThunk, placeOrderThunk } from './thunk'; import { useSelector } from 'react-redux'; import uuid from 'react-native-uuid'; import { v4 } from 'react-native-uuid/dist/v4'; +import { useStripePayment } from './hooks/useStripePayment'; type CheckoutPaymentNavProp = StackNavigationProp< AppStackParamList, @@ -52,25 +53,15 @@ export const CheckoutPaymentScreen: React.FC = () => { // console.log(uuid); const { paymentMethods, - placeOrderSuccess, placeOrderLoading, - placeOrderError, } = useAppSelector((state: RootState) => state.paymentMethods); + const { processCardPayment, isPaymentProcessing } = useStripePayment(); + useEffect(() => { dispatch(getAllPaymentMethodsThunk()); }, [dispatch]); - useEffect(() => { - if (placeOrderSuccess) { - navigation.navigate('OrderConfirmedScreen', { - orderId: 'ORD-' + Math.floor(Math.random() * 900000 + 100000), - }); - } else if (placeOrderError) { - Alert.alert('Error', placeOrderError); - } - }, [placeOrderSuccess, placeOrderError]); - // console.log('paymentMethods', paymentMethods); const selectedPaymentMethod = paymentMethods.find( @@ -84,18 +75,43 @@ export const CheckoutPaymentScreen: React.FC = () => { // },); - const handlePlaceOrder = () => { - dispatch( - placeOrderThunk({ - addressId: selectedAddressId || '', - paymentMethodId: selectedMethod, - paymentMethod: methodName || '', - orderType: 'DELIVERY', - idempotencyKey: `idemp-key-${uuid}`, - }), - ); + const handlePlaceOrder = async () => { + try { + const response = await dispatch( + placeOrderThunk({ + addressId: selectedAddressId || '', + paymentMethodId: selectedMethod, + paymentMethod: methodName || '', + orderType: 'DELIVERY', + idempotencyKey: `idemp-key-${uuid}`, + }), + ).unwrap(); + + if (methodName === 'CARD') { + // Delegate entire Stripe flow to the hook + const success = await processCardPayment(response); + if (success) { + navigation.navigate('OrderConfirmedScreen', { + orderId: + response.orders[0]?.orderNumber || + 'ORD-' + Math.floor(Math.random() * 900000 + 100000), + }); + } + } else { + // Non-card methods (COD, UPI, Wallet) – navigate directly + navigation.navigate('OrderConfirmedScreen', { + orderId: + response.orders[0]?.orderNumber || + 'ORD-' + Math.floor(Math.random() * 900000 + 100000), + }); + } + } catch (err: any) { + Alert.alert('Error', err || 'Failed to place order'); + } }; + const isLoading = placeOrderLoading || isPaymentProcessing; + return (
navigation.goBack()} /> @@ -114,9 +130,9 @@ export const CheckoutPaymentScreen: React.FC = () => { diff --git a/app/features/screens/checkoutPaymentScreen/hooks/useStripePayment.ts b/app/features/screens/checkoutPaymentScreen/hooks/useStripePayment.ts new file mode 100644 index 0000000..7c7d598 --- /dev/null +++ b/app/features/screens/checkoutPaymentScreen/hooks/useStripePayment.ts @@ -0,0 +1,102 @@ +import { useState, useCallback } from 'react'; +import { Alert } from 'react-native'; +import { useStripe } from '@stripe/stripe-react-native'; +import { PlaceOrderResponse } from '@interfaces'; +import { verifyPaymentApi } from '@api'; + +/** + * Custom hook that encapsulates the entire Stripe card payment flow: + * 1. Extract session info from the place-order response + * 2. Initialize the Stripe Payment Sheet + * 3. Present the Payment Sheet to the user + * 4. Verify the payment on the backend via /payments/process + * + * Returns: + * - processCardPayment: async function to run the full flow + * - isPaymentProcessing: loading flag for UI + */ +export const useStripePayment = () => { + const { initPaymentSheet, presentPaymentSheet } = useStripe(); + const [isPaymentProcessing, setIsPaymentProcessing] = useState(false); + + /** + * Runs the complete Stripe card payment flow. + * @param response – the PlaceOrderResponse from the backend (contains checkoutSessions) + * @returns `true` if payment was verified successfully, `false` otherwise + */ + const processCardPayment = useCallback( + async (response: PlaceOrderResponse): Promise => { + try { + setIsPaymentProcessing(true); + + // ── 1. Extract session details ───────────────────────────────────── + const session = response.checkoutSessions?.[0]; + // console.log('session', session); + const clientSecret = session?.gatewayToken; + // console.log('clientSecret', clientSecret); + const gatewayOrderId = session?.gatewayOrderId; + // console.log('gatewayOrderId', gatewayOrderId); + + if (!clientSecret) { + Alert.alert('Error', 'Payment session token not found.'); + return false; + } + + // ── 2. Initialize Payment Sheet ──────────────────────────────────── + const { error: initError } = await initPaymentSheet({ + paymentIntentClientSecret: clientSecret, + merchantDisplayName: 'SG Delivery', + }); + + if (initError) { + Alert.alert('Error', `Stripe init failed: ${initError.message}`); + return false; + } + + // ── 3. Present Payment Sheet ─────────────────────────────────────── + const { error: presentError } = await presentPaymentSheet(); + + if (presentError) { + if (presentError.code === 'Canceled') { + Alert.alert( + 'Payment Cancelled', + 'You cancelled the payment process.', + ); + } else { + console.log(presentError.message); + Alert.alert('Payment Error', presentError.message); + } + return false; + } + + // ── 4. Verify payment on backend ─────────────────────────────────── + const verifyRes = await verifyPaymentApi({ + gatewayOrderId: gatewayOrderId || '', + gatewayPaymentId: gatewayOrderId || '', + gatewaySignature: 'stripe_signature_verified', + }); + + if (verifyRes.success) { + return true; + } else { + Alert.alert( + 'Verification Failed', + verifyRes.message || 'Unable to confirm payment.', + ); + return false; + } + } catch (error: any) { + Alert.alert( + 'Payment Error', + error?.message || 'Something went wrong during payment.', + ); + return false; + } finally { + setIsPaymentProcessing(false); + } + }, + [initPaymentSheet, presentPaymentSheet], + ); + + return { processCardPayment, isPaymentProcessing }; +}; diff --git a/app/interfaces/order.ts b/app/interfaces/order.ts index f45c7b6..6abd5ad 100644 --- a/app/interfaces/order.ts +++ b/app/interfaces/order.ts @@ -12,6 +12,16 @@ export interface PlaceOrderResponse { success: boolean; message: string; orders: Order[]; + checkoutSessions: CheckoutSession[]; +} + +export interface CheckoutSession { + orderId: string; + orderNumber: string; + paymentId: string; + gatewayOrderId: string; + gatewayToken: string; + amount: number; } export interface Order {