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 { AppStackParamList } from '../../../navigation/appStack';
|
||||||
import { RootState, useAppDispatch, useAppSelector } from '@store';
|
import { RootState, useAppDispatch, useAppSelector } from '@store';
|
||||||
import { getAllPaymentMethodsThunk, placeOrderThunk } from './thunk';
|
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 { v4 } from 'react-native-uuid/dist/v4';
|
||||||
import { useStripePayment } from './hooks/useStripePayment';
|
import { useStripePayment } from './hooks/useStripePayment';
|
||||||
|
import { useRazorPayment } from './hooks/useRazorPayment';
|
||||||
|
|
||||||
type CheckoutPaymentNavProp = StackNavigationProp<
|
type CheckoutPaymentNavProp = StackNavigationProp<
|
||||||
AppStackParamList,
|
AppStackParamList,
|
||||||
@ -33,12 +32,12 @@ const CHECKOUT_STEPS = [
|
|||||||
{ key: 'confirm', label: 'Confirm' },
|
{ key: 'confirm', label: 'Confirm' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const PAYMENT_METHODS = [
|
// const PAYMENT_METHODS = [
|
||||||
{ id: 'upi', type: 'UPI' as const, label: 'UPI (Google Pay, PhonePe)' },
|
// { id: 'upi', type: 'UPI' as const, label: 'UPI (Google Pay, PhonePe)' },
|
||||||
{ id: 'card', type: 'Card' as const, label: 'Credit / Debit Card' },
|
// { id: 'card', type: 'Card' as const, label: 'Credit / Debit Card' },
|
||||||
{ id: 'wallet', type: 'Wallet' as const, label: 'Wallet' },
|
// { id: 'wallet', type: 'Wallet' as const, label: 'Wallet' },
|
||||||
{ id: 'cod', type: 'COD' as const, label: 'Cash on Delivery' },
|
// { id: 'cod', type: 'COD' as const, label: 'Cash on Delivery' },
|
||||||
];
|
// ];
|
||||||
|
|
||||||
export const CheckoutPaymentScreen: React.FC = () => {
|
export const CheckoutPaymentScreen: React.FC = () => {
|
||||||
const { colors } = useAppTheme();
|
const { colors } = useAppTheme();
|
||||||
@ -51,12 +50,13 @@ export const CheckoutPaymentScreen: React.FC = () => {
|
|||||||
// console.log(selectedAddressId);
|
// console.log(selectedAddressId);
|
||||||
const uuid = v4();
|
const uuid = v4();
|
||||||
// console.log(uuid);
|
// console.log(uuid);
|
||||||
const {
|
const { paymentMethods, placeOrderLoading } = useAppSelector(
|
||||||
paymentMethods,
|
(state: RootState) => state.paymentMethods,
|
||||||
placeOrderLoading,
|
);
|
||||||
} = useAppSelector((state: RootState) => state.paymentMethods);
|
|
||||||
|
|
||||||
const { processCardPayment, isPaymentProcessing } = useStripePayment();
|
const { processCardPayment, isPaymentProcessing } = useStripePayment();
|
||||||
|
const { processRazorpayPayment, isPaymentProcessing: isRazorpayProcessing } =
|
||||||
|
useRazorPayment();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(getAllPaymentMethodsThunk());
|
dispatch(getAllPaymentMethodsThunk());
|
||||||
@ -97,8 +97,18 @@ export const CheckoutPaymentScreen: React.FC = () => {
|
|||||||
'ORD-' + Math.floor(Math.random() * 900000 + 100000),
|
'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 {
|
} else {
|
||||||
// Non-card methods (COD, UPI, Wallet) – navigate directly
|
// Non-card methods (COD, Wallet) – navigate directly
|
||||||
navigation.navigate('OrderConfirmedScreen', {
|
navigation.navigate('OrderConfirmedScreen', {
|
||||||
orderId:
|
orderId:
|
||||||
response.orders[0]?.orderNumber ||
|
response.orders[0]?.orderNumber ||
|
||||||
@ -110,7 +120,8 @@ export const CheckoutPaymentScreen: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isLoading = placeOrderLoading || isPaymentProcessing;
|
const isLoading =
|
||||||
|
placeOrderLoading || isPaymentProcessing || isRazorpayProcessing;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<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