feat: implement payment checkout screen, Razorpay hook, and add documentation for tracking and payment integration
This commit is contained in:
parent
022fa69c0a
commit
6259c5b740
@ -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}>
|
||||
|
||||
@ -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 };
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user