feat: implement payment processing with Stripe and add documentation for tracking and payment integration.
This commit is contained in:
parent
5b8bc3d88a
commit
022fa69c0a
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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
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 { 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>
|
||||||
|
|||||||
@ -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;
|
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 {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user