feat: implement payment processing with Stripe and add documentation for tracking and payment integration.

This commit is contained in:
Tamojit Biswas 2026-07-17 14:16:10 +05:30
parent 5b8bc3d88a
commit 022fa69c0a
6 changed files with 187 additions and 27 deletions

View File

@ -11,14 +11,18 @@ import { persistor, store } from './store';
import { RootNavigator } from './navigation/rootNavigator'; import { RootNavigator } from './navigation/rootNavigator';
import { PersistGate } from 'redux-persist/integration/react'; import { PersistGate } from 'redux-persist/integration/react';
import { colors } from '@theme'; import { colors } from '@theme';
import { StripeProvider } from '@stripe/stripe-react-native';
import { ENV } from './config';
function App() { function App() {
return ( return (
<Provider store={store}> <Provider store={store}>
<PersistGate loading={null} persistor={persistor}> <PersistGate loading={null} persistor={persistor}>
<StripeProvider publishableKey={ENV.STRIPE_PUBLISHABLE_KEY}>
<SafeAreaProvider> <SafeAreaProvider>
<AppContent /> <AppContent />
</SafeAreaProvider> </SafeAreaProvider>
</StripeProvider>
</PersistGate> </PersistGate>
</Provider> </Provider>
); );

View File

@ -4,3 +4,21 @@ import { apiClient } from '@services';
export const getAllPaymentMethodsApi = async () => { export const getAllPaymentMethodsApi = async () => {
return await apiClient.get<PaymentMethodsResponse>('/payments/options'); return await apiClient.get<PaymentMethodsResponse>('/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<VerifyPaymentResponse>(
'/payments/process',
payload,
);
};

10
app/config/index.ts Normal file
View File

@ -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',
};

View File

@ -16,6 +16,7 @@ import { getAllPaymentMethodsThunk, placeOrderThunk } from './thunk';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import uuid from 'react-native-uuid'; 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';
type CheckoutPaymentNavProp = StackNavigationProp< type CheckoutPaymentNavProp = StackNavigationProp<
AppStackParamList, AppStackParamList,
@ -52,25 +53,15 @@ export const CheckoutPaymentScreen: React.FC = () => {
// console.log(uuid); // console.log(uuid);
const { const {
paymentMethods, paymentMethods,
placeOrderSuccess,
placeOrderLoading, placeOrderLoading,
placeOrderError,
} = useAppSelector((state: RootState) => state.paymentMethods); } = useAppSelector((state: RootState) => state.paymentMethods);
const { processCardPayment, isPaymentProcessing } = useStripePayment();
useEffect(() => { useEffect(() => {
dispatch(getAllPaymentMethodsThunk()); dispatch(getAllPaymentMethodsThunk());
}, [dispatch]); }, [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); // console.log('paymentMethods', paymentMethods);
const selectedPaymentMethod = paymentMethods.find( const selectedPaymentMethod = paymentMethods.find(
@ -84,8 +75,9 @@ export const CheckoutPaymentScreen: React.FC = () => {
// },); // },);
const handlePlaceOrder = () => { const handlePlaceOrder = async () => {
dispatch( try {
const response = await dispatch(
placeOrderThunk({ placeOrderThunk({
addressId: selectedAddressId || '', addressId: selectedAddressId || '',
paymentMethodId: selectedMethod, paymentMethodId: selectedMethod,
@ -93,9 +85,33 @@ export const CheckoutPaymentScreen: React.FC = () => {
orderType: 'DELIVERY', orderType: 'DELIVERY',
idempotencyKey: `idemp-key-${uuid}`, 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 ( return (
<View style={styles.container}> <View style={styles.container}>
<Header title="Checkout" onBack={() => navigation.goBack()} /> <Header title="Checkout" onBack={() => navigation.goBack()} />
@ -114,9 +130,9 @@ export const CheckoutPaymentScreen: React.FC = () => {
</ScrollView> </ScrollView>
<View style={styles.footer}> <View style={styles.footer}>
<PrimaryButton <PrimaryButton
title={placeOrderLoading ? 'Placing...' : 'Place Order'} title={isLoading ? 'Processing...' : 'Place Order'}
onPress={handlePlaceOrder} onPress={handlePlaceOrder}
disabled={placeOrderLoading} disabled={isLoading}
/> />
</View> </View>
</View> </View>

View File

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

View File

@ -12,6 +12,16 @@ export interface PlaceOrderResponse {
success: boolean; success: boolean;
message: string; message: string;
orders: Order[]; orders: Order[];
checkoutSessions: CheckoutSession[];
}
export interface CheckoutSession {
orderId: string;
orderNumber: string;
paymentId: string;
gatewayOrderId: string;
gatewayToken: string;
amount: number;
} }
export interface Order { export interface Order {