From 6259c5b74077580e3fce7bf7e00cbf54415131e5 Mon Sep 17 00:00:00 2001 From: Tamojit Biswas Date: Fri, 17 Jul 2026 17:02:06 +0530 Subject: [PATCH] feat: implement payment checkout screen, Razorpay hook, and add documentation for tracking and payment integration --- .../checkoutPaymentScreen.tsx | 39 ++++-- .../hooks/useRazorPayment.ts | 120 ++++++++++++++++++ 2 files changed, 145 insertions(+), 14 deletions(-) create mode 100644 app/features/screens/checkoutPaymentScreen/hooks/useRazorPayment.ts diff --git a/app/features/screens/checkoutPaymentScreen/checkoutPaymentScreen.tsx b/app/features/screens/checkoutPaymentScreen/checkoutPaymentScreen.tsx index 9468b03..ceeb3e4 100644 --- a/app/features/screens/checkoutPaymentScreen/checkoutPaymentScreen.tsx +++ b/app/features/screens/checkoutPaymentScreen/checkoutPaymentScreen.tsx @@ -13,10 +13,9 @@ import { useAppTheme } from '@theme'; import { AppStackParamList } from '../../../navigation/appStack'; import { RootState, useAppDispatch, useAppSelector } from '@store'; 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'; +import { useRazorPayment } from './hooks/useRazorPayment'; type CheckoutPaymentNavProp = StackNavigationProp< AppStackParamList, @@ -33,12 +32,12 @@ const CHECKOUT_STEPS = [ { key: 'confirm', label: 'Confirm' }, ]; -const PAYMENT_METHODS = [ - { id: 'upi', type: 'UPI' as const, label: 'UPI (Google Pay, PhonePe)' }, - { id: 'card', type: 'Card' as const, label: 'Credit / Debit Card' }, - { id: 'wallet', type: 'Wallet' as const, label: 'Wallet' }, - { id: 'cod', type: 'COD' as const, label: 'Cash on Delivery' }, -]; +// const PAYMENT_METHODS = [ +// { id: 'upi', type: 'UPI' as const, label: 'UPI (Google Pay, PhonePe)' }, +// { id: 'card', type: 'Card' as const, label: 'Credit / Debit Card' }, +// { id: 'wallet', type: 'Wallet' as const, label: 'Wallet' }, +// { id: 'cod', type: 'COD' as const, label: 'Cash on Delivery' }, +// ]; export const CheckoutPaymentScreen: React.FC = () => { const { colors } = useAppTheme(); @@ -51,12 +50,13 @@ export const CheckoutPaymentScreen: React.FC = () => { // console.log(selectedAddressId); const uuid = v4(); // console.log(uuid); - const { - paymentMethods, - placeOrderLoading, - } = useAppSelector((state: RootState) => state.paymentMethods); + const { paymentMethods, placeOrderLoading } = useAppSelector( + (state: RootState) => state.paymentMethods, + ); const { processCardPayment, isPaymentProcessing } = useStripePayment(); + const { processRazorpayPayment, isPaymentProcessing: isRazorpayProcessing } = + useRazorPayment(); useEffect(() => { dispatch(getAllPaymentMethodsThunk()); @@ -97,8 +97,18 @@ export const CheckoutPaymentScreen: React.FC = () => { 'ORD-' + Math.floor(Math.random() * 900000 + 100000), }); } + } else if (methodName === 'UPI') { + // Delegate entire Razorpay UPI flow to the hook + const success = await processRazorpayPayment(response, 'upi'); + 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 + // Non-card methods (COD, Wallet) – navigate directly navigation.navigate('OrderConfirmedScreen', { orderId: response.orders[0]?.orderNumber || @@ -110,7 +120,8 @@ export const CheckoutPaymentScreen: React.FC = () => { } }; - const isLoading = placeOrderLoading || isPaymentProcessing; + const isLoading = + placeOrderLoading || isPaymentProcessing || isRazorpayProcessing; return ( diff --git a/app/features/screens/checkoutPaymentScreen/hooks/useRazorPayment.ts b/app/features/screens/checkoutPaymentScreen/hooks/useRazorPayment.ts new file mode 100644 index 0000000..f2df90e --- /dev/null +++ b/app/features/screens/checkoutPaymentScreen/hooks/useRazorPayment.ts @@ -0,0 +1,120 @@ +import { useState, useCallback } from 'react'; +import { Alert } from 'react-native'; +// @ts-ignore +import RazorpayCheckout from 'react-native-razorpay'; +import { PlaceOrderResponse } from '@interfaces'; +import { verifyPaymentApi } from '@api'; +import { useAppSelector } from '@store'; +import { ENV } from '../../../../config'; + +/** + * Custom hook that encapsulates the entire Razorpay UPI payment flow: + * 1. Extract session details (gatewayOrderId, amount) from the place-order response + * 2. Configure Razorpay checkout options with prefilled customer profile details + * 3. Present the Razorpay Checkout overlay to the user + * 4. Verify the payment signature on the backend via /payments/process + * + * Returns: + * - processRazorpayPayment: async function to execute the Razorpay flow + * - isPaymentProcessing: loading flag for UPI payment step + */ +export const useRazorPayment = () => { + const [isPaymentProcessing, setIsPaymentProcessing] = useState(false); + const user = useAppSelector( + state => state.customerProfile.customerDetails?.user, + ); + + /** + * Runs the complete Razorpay payment flow. + * @param response – the PlaceOrderResponse from the backend + * @returns `true` if payment was completed and verified successfully, `false` otherwise + */ + const processRazorpayPayment = useCallback( + async ( + response: PlaceOrderResponse, + paymentMethod: 'upi' | 'card' | undefined = undefined, + ): Promise => { + try { + setIsPaymentProcessing(true); + + // ── 1. Extract session details ───────────────────────────────────── + const session = response.checkoutSessions?.[0]; + if (!session) { + Alert.alert('Error', 'Payment session not found.'); + return false; + } + + const { gatewayOrderId, amount } = session; + if (!gatewayOrderId) { + Alert.alert('Error', 'Razorpay Order ID not found.'); + return false; + } + + // Amount in checkoutSessions represents standard rupees. + // Fallback to first order's total amount if needed. + const amountVal = + amount ?? parseFloat(response.orders?.[0]?.totalAmount || '0'); + // Razorpay SDK requires the amount in the smallest currency sub-units (paise for INR) + const amountInPaise = Math.round(amountVal * 100); + + // ── 2. Configure Checkout Options ────────────────────────────────── + const options = { + key: ENV.RAZORPAY_KEY_ID, + amount: amountInPaise, + currency: 'INR', + name: 'SG Delivery', + description: `Payment for Order #${ + session.orderNumber || response.orders?.[0]?.orderNumber || '' + }`, + order_id: gatewayOrderId, + method: 'upi', + prefill: { + name: user?.name || '', + email: user?.email || '', + contact: user?.phone || '', + }, + theme: { color: '#05824C' }, // Matching brand green primary color + }; + + // ── 3. Present Razorpay Checkout SDK ─────────────────────────────── + const rzpData = await RazorpayCheckout.open(options); + + // ── 4. Verify payment cryptographically on backend ──────────────── + const verifyRes = await verifyPaymentApi({ + gatewayOrderId: rzpData.razorpay_order_id || gatewayOrderId, + gatewayPaymentId: rzpData.razorpay_payment_id, + gatewaySignature: rzpData.razorpay_signature, + }); + + if (verifyRes.success) { + return true; + } else { + Alert.alert( + 'Verification Failed', + verifyRes.message || 'Unable to confirm payment.', + ); + return false; + } + } catch (error: any) { + // Razorpay Checkout library throws an error if user cancels or transaction fails + if (error && error.code) { + Alert.alert( + 'Payment Cancelled', + error.description || 'You cancelled the payment process.', + ); + } else { + Alert.alert( + 'Payment Error', + error?.message || 'Something went wrong during payment.', + ); + } + return false; + } finally { + setIsPaymentProcessing(false); + } + }, + [user], + ); + + return { processRazorpayPayment, isPaymentProcessing }; +};