feat: implement payment processing with Stripe and add documentation for tracking and payment integration.
This commit is contained in:
parent
5b8bc3d88a
commit
022fa69c0a
10
app/App.tsx
10
app/App.tsx
@ -11,14 +11,18 @@ import { persistor, store } from './store';
|
||||
import { RootNavigator } from './navigation/rootNavigator';
|
||||
import { PersistGate } from 'redux-persist/integration/react';
|
||||
import { colors } from '@theme';
|
||||
import { StripeProvider } from '@stripe/stripe-react-native';
|
||||
import { ENV } from './config';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<SafeAreaProvider>
|
||||
<AppContent />
|
||||
</SafeAreaProvider>
|
||||
<StripeProvider publishableKey={ENV.STRIPE_PUBLISHABLE_KEY}>
|
||||
<SafeAreaProvider>
|
||||
<AppContent />
|
||||
</SafeAreaProvider>
|
||||
</StripeProvider>
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
@ -4,3 +4,21 @@ import { apiClient } from '@services';
|
||||
export const getAllPaymentMethodsApi = async () => {
|
||||
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
10
app/config/index.ts
Normal 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',
|
||||
};
|
||||
@ -16,6 +16,7 @@ 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';
|
||||
|
||||
type CheckoutPaymentNavProp = StackNavigationProp<
|
||||
AppStackParamList,
|
||||
@ -52,25 +53,15 @@ export const CheckoutPaymentScreen: React.FC = () => {
|
||||
// console.log(uuid);
|
||||
const {
|
||||
paymentMethods,
|
||||
placeOrderSuccess,
|
||||
placeOrderLoading,
|
||||
placeOrderError,
|
||||
} = useAppSelector((state: RootState) => state.paymentMethods);
|
||||
|
||||
const { processCardPayment, isPaymentProcessing } = useStripePayment();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getAllPaymentMethodsThunk());
|
||||
}, [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);
|
||||
|
||||
const selectedPaymentMethod = paymentMethods.find(
|
||||
@ -84,18 +75,43 @@ export const CheckoutPaymentScreen: React.FC = () => {
|
||||
|
||||
// },);
|
||||
|
||||
const handlePlaceOrder = () => {
|
||||
dispatch(
|
||||
placeOrderThunk({
|
||||
addressId: selectedAddressId || '',
|
||||
paymentMethodId: selectedMethod,
|
||||
paymentMethod: methodName || '',
|
||||
orderType: 'DELIVERY',
|
||||
idempotencyKey: `idemp-key-${uuid}`,
|
||||
}),
|
||||
);
|
||||
const handlePlaceOrder = async () => {
|
||||
try {
|
||||
const response = await dispatch(
|
||||
placeOrderThunk({
|
||||
addressId: selectedAddressId || '',
|
||||
paymentMethodId: selectedMethod,
|
||||
paymentMethod: methodName || '',
|
||||
orderType: 'DELIVERY',
|
||||
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 (
|
||||
<View style={styles.container}>
|
||||
<Header title="Checkout" onBack={() => navigation.goBack()} />
|
||||
@ -114,9 +130,9 @@ export const CheckoutPaymentScreen: React.FC = () => {
|
||||
</ScrollView>
|
||||
<View style={styles.footer}>
|
||||
<PrimaryButton
|
||||
title={placeOrderLoading ? 'Placing...' : 'Place Order'}
|
||||
title={isLoading ? 'Processing...' : 'Place Order'}
|
||||
onPress={handlePlaceOrder}
|
||||
disabled={placeOrderLoading}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@ -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 };
|
||||
};
|
||||
@ -12,6 +12,16 @@ export interface PlaceOrderResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
orders: Order[];
|
||||
checkoutSessions: CheckoutSession[];
|
||||
}
|
||||
|
||||
export interface CheckoutSession {
|
||||
orderId: string;
|
||||
orderNumber: string;
|
||||
paymentId: string;
|
||||
gatewayOrderId: string;
|
||||
gatewayToken: string;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export interface Order {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user