feat: implement payment checkout screen, Razorpay hook, and add documentation for tracking and payment integration

This commit is contained in:
Tamojit Biswas 2026-07-17 17:02:06 +05:30
parent 022fa69c0a
commit 6259c5b740
2 changed files with 145 additions and 14 deletions

View File

@ -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 (
<View style={styles.container}>

View File

@ -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<boolean> => {
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 };
};