diff --git a/.env b/.env index 31a3900..ac397ab 100644 --- a/.env +++ b/.env @@ -1,3 +1,4 @@ STRIPE_SECRET_KEY=sk_test_51Ox04DSHlFQYe8R5HXy6nj0eQqtqAP4ynF7ODFg71ork78B38MPsDV3gQEo2EYaFL9OG75L8tG7bKxptsmVeONrS00ji44eUIl +STRIPE_PUBLISHABLE_KEY=pk_test_51Ox04DSHlFQYe8R5Cvm8i6n99QynUNj7WQzACB89PImnt0X8Z54SBFM22ghSNHYFo7OgVcyba9QhhyrrdRqRPJpF00Us8XJv7f RAZORPAY_KEY_ID=rzp_test_TCxbW8AxcXgMCj RAZORPAY_KEY_SECRET=rLlM87yFehhsVNSqjCE2qRz5 \ No newline at end of file diff --git a/app/components/OrderStatusTimeline/OrderStatusTimeline.tsx b/app/components/OrderStatusTimeline/OrderStatusTimeline.tsx index 81c782c..49b461d 100644 --- a/app/components/OrderStatusTimeline/OrderStatusTimeline.tsx +++ b/app/components/OrderStatusTimeline/OrderStatusTimeline.tsx @@ -206,8 +206,6 @@ export const OrderStatusTimeline: React.FC = ({ ]; const showDriver = [ - 'PREPARING', - 'READY_FOR_PICKUP', 'OUT_FOR_DELIVERY', 'DELIVERED', ].includes(currentStatus); diff --git a/app/features/screens/checkoutAddressScreen/checkoutAddressScreen.tsx b/app/features/screens/checkoutAddressScreen/checkoutAddressScreen.tsx index 28f6651..64e0897 100644 --- a/app/features/screens/checkoutAddressScreen/checkoutAddressScreen.tsx +++ b/app/features/screens/checkoutAddressScreen/checkoutAddressScreen.tsx @@ -32,7 +32,7 @@ export const CheckoutAddressScreen: React.FC = () => { const navigation = useNavigation(); const { customerDetails } = useAppSelector(state => state.customerProfile); - const addresses: CustomerAddress[] = customerDetails?.addresses ?? []; + const addresses: CustomerAddress[] = useMemo(() => customerDetails?.addresses ?? [], [customerDetails]); const defaultAddressId = useMemo(() => { const def = addresses.find(a => a.isDefault); diff --git a/app/features/screens/checkoutPaymentScreen/checkoutPaymentScreen.tsx b/app/features/screens/checkoutPaymentScreen/checkoutPaymentScreen.tsx index ceeb3e4..7b4785b 100644 --- a/app/features/screens/checkoutPaymentScreen/checkoutPaymentScreen.tsx +++ b/app/features/screens/checkoutPaymentScreen/checkoutPaymentScreen.tsx @@ -109,7 +109,7 @@ export const CheckoutPaymentScreen: React.FC = () => { } } else { // Non-card methods (COD, Wallet) – navigate directly - navigation.navigate('OrderConfirmedScreen', { + navigation.replace('OrderConfirmedScreen', { orderId: response.orders[0]?.orderNumber || 'ORD-' + Math.floor(Math.random() * 900000 + 100000), diff --git a/app/features/screens/completeProfileScreen/completeProfileScreen.tsx b/app/features/screens/completeProfileScreen/completeProfileScreen.tsx index 186f38a..92a3841 100644 --- a/app/features/screens/completeProfileScreen/completeProfileScreen.tsx +++ b/app/features/screens/completeProfileScreen/completeProfileScreen.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { View, Text, @@ -12,8 +12,7 @@ import { StackNavigationProp } from '@react-navigation/stack'; import { getStyles } from './completeProfileScreen.styles'; import { CustomInput, PrimaryButton } from '@components'; import { useAppTheme } from '@theme'; -import { AuthStackParamList } from '../../../navigation/authStack'; -import { useAppDispatch } from '@store'; +import { useAppDispatch, useAppSelector } from '@store'; import { saveProfileData } from './reducer'; import { OnboardingStackParamList } from '@navigation/onboardingStack'; @@ -44,6 +43,14 @@ export const CompleteProfileScreen: React.FC = () => { const [selectedLabel, setSelectedLabel] = useState('Home'); + const locationData = useAppSelector(state => state.setLocation); + + useEffect(() => { + if (locationData.city) setCity(locationData.city); + if (locationData.state) setState(locationData.state); + if (locationData.postalCode) setPostalCode(locationData.postalCode); + }, [locationData]); + const handleSave = () => { dispatch( saveProfileData({ diff --git a/app/features/screens/homeScreen/homeScreen.tsx b/app/features/screens/homeScreen/homeScreen.tsx index 1ef0f32..242db19 100644 --- a/app/features/screens/homeScreen/homeScreen.tsx +++ b/app/features/screens/homeScreen/homeScreen.tsx @@ -30,6 +30,7 @@ import { useAppSelector, } from '@store'; import { getAllCategoriesThunk, getAllProductsThunk } from './thunk'; +import { getCategoryEmoji } from '@utils'; type NavProp = CompositeNavigationProp< BottomTabNavigationProp, @@ -41,18 +42,7 @@ const BANNER_STEP = width - 32 + 12; // card width + margin const ALL_CATEGORY = { id: 'all', name: 'All', slug: 'all', imageUrl: null, isActive: true, parentId: null }; -const getCategoryEmoji = (name: string): string => { - const n = name.toLowerCase(); - if (n.includes('food')) return 'πŸ”'; - if (n.includes('grocer')) return 'πŸ›’'; - if (n.includes('pharma') || n.includes('medic')) return 'πŸ’Š'; - if (n.includes('meat')) return 'πŸ₯©'; - if (n.includes('flower')) return 'πŸ’'; - if (n.includes('electronic')) return 'πŸ“±'; - if (n.includes('cloth') || n.includes('fashion')) return 'πŸ‘—'; - if (n.includes('bakery') || n.includes('cake')) return 'πŸŽ‚'; - return 'πŸ“¦'; -}; + const PROMOS = [ { @@ -90,7 +80,7 @@ export const HomeScreen: React.FC = () => { const { products, isLoading, categories } = useAppSelector( state => state.home, ); - + const { customerDetails } = useAppSelector(state => state.customerProfile); const [selectedCategory, setSelectedCategory] = useState('all'); const [activeBanner, setActiveBanner] = useState(0); @@ -137,7 +127,7 @@ export const HomeScreen: React.FC = () => { Deliver to - Koramangala 4th Block + {customerDetails?.addresses[0]?.mapAddress} β–Ό @@ -158,7 +148,7 @@ export const HomeScreen: React.FC = () => { {}} + onChangeText={() => { }} placeholder="Search providers or items..." onFocus={handleSearchFocus} /> diff --git a/app/features/screens/orderConfirmedScreen/orderConfirmedScreen.tsx b/app/features/screens/orderConfirmedScreen/orderConfirmedScreen.tsx index 43a8e84..54b30ec 100644 --- a/app/features/screens/orderConfirmedScreen/orderConfirmedScreen.tsx +++ b/app/features/screens/orderConfirmedScreen/orderConfirmedScreen.tsx @@ -32,7 +32,7 @@ export const OrderConfirmedScreen: React.FC = () => { - navigation.navigate('OrderTrackingScreen', { orderId })} /> + navigation.replace('MainTabs')} /> ); diff --git a/app/features/screens/otpScreen/hooks/useOtpScreen.ts b/app/features/screens/otpScreen/hooks/useOtpScreen.ts index b619e60..2f8b8bd 100644 --- a/app/features/screens/otpScreen/hooks/useOtpScreen.ts +++ b/app/features/screens/otpScreen/hooks/useOtpScreen.ts @@ -77,7 +77,7 @@ export const useOtpScreen = () => { verifyOtp({ phone: mobileNumber, code: otpString, role: 'CUSTOMER' }), ).unwrap(); - navigation.navigate('SetLocationScreen'); + // navigation.navigate('SetLocationScreen'); } catch (error) { const message = typeof error === 'string' ? error : 'Invalid OTP. Please try again.'; diff --git a/app/features/screens/setLocationScreen/reducer.ts b/app/features/screens/setLocationScreen/reducer.ts index f7dc29e..98bf3dd 100644 --- a/app/features/screens/setLocationScreen/reducer.ts +++ b/app/features/screens/setLocationScreen/reducer.ts @@ -6,6 +6,9 @@ export const setLocationData = createAction<{ latitude: number; longitude: number; mapAddress: string; + city: string; + state: string; + postalCode: string; }>('setLocation/setLocationData'); export const clearLocationData = createAction('setLocation/clearLocationData'); @@ -16,12 +19,18 @@ export interface SetLocationState { latitude: number | null; longitude: number | null; mapAddress: string; + city: string; + state: string; + postalCode: string; } const initialState: SetLocationState = { latitude: null, longitude: null, mapAddress: '', + city: '', + state: '', + postalCode: '', }; // ─── Reducer ────────────────────────────────────────────────────────────────── @@ -32,11 +41,17 @@ const setLocationReducer = createReducer(initialState, builder => { state.latitude = action.payload.latitude; state.longitude = action.payload.longitude; state.mapAddress = action.payload.mapAddress; + state.city = action.payload.city; + state.state = action.payload.state; + state.postalCode = action.payload.postalCode; }) .addCase(clearLocationData, state => { state.latitude = null; state.longitude = null; state.mapAddress = ''; + state.city = ''; + state.state = ''; + state.postalCode = ''; }); }); diff --git a/app/features/screens/setLocationScreen/setLocationScreen.tsx b/app/features/screens/setLocationScreen/setLocationScreen.tsx index a401953..1bcda5d 100644 --- a/app/features/screens/setLocationScreen/setLocationScreen.tsx +++ b/app/features/screens/setLocationScreen/setLocationScreen.tsx @@ -12,7 +12,6 @@ import { StackNavigationProp } from '@react-navigation/stack'; import { getStyles } from './setLocationScreen.styles'; import { PrimaryButton } from '@components'; import { useAppTheme } from '@theme'; -import { AuthStackParamList } from '../../../navigation/authStack'; import { DEFAULT_LOCATION, getCurrentLocationWithAddress, @@ -42,6 +41,9 @@ export const SetLocationScreen: React.FC = () => { const [coords, setCoords] = useState(DEFAULT_LOCATION); const [address, setAddress] = useState('Fetching your location…'); + const [city, setCity] = useState(''); + const [state, setState] = useState(''); + const [postalCode, setPostalCode] = useState(''); const [loading, setLoading] = useState(true); // ------------------------------------------------------------------ @@ -57,6 +59,9 @@ export const SetLocationScreen: React.FC = () => { if (!isMounted.current) return; setCoords(result.coords); setAddress(result.address); + setCity(result.city || ''); + setState(result.state || ''); + setPostalCode(result.postalCode || ''); // Animate the map to the user's location mapRef.current?.animateToRegion({ ...result.coords, ...DELTA }, 600); @@ -97,9 +102,15 @@ export const SetLocationScreen: React.FC = () => { }; setCoords(newCoords); setAddress('Fetching address…'); + setCity(''); + setState(''); + setPostalCode(''); - const newAddress = await reverseGeocode(newCoords); - setAddress(newAddress); + const geocodeResult = await reverseGeocode(newCoords); + setAddress(geocodeResult.address); + setCity(geocodeResult.city); + setState(geocodeResult.state); + setPostalCode(geocodeResult.postalCode); }, []); return ( @@ -162,6 +173,9 @@ export const SetLocationScreen: React.FC = () => { latitude: coords.latitude, longitude: coords.longitude, mapAddress: address, + city, + state, + postalCode, }), ); navigation.navigate('CompleteProfileScreen'); diff --git a/app/services/apiClient.ts b/app/services/apiClient.ts index 3699a6a..52c2cd9 100644 --- a/app/services/apiClient.ts +++ b/app/services/apiClient.ts @@ -15,7 +15,7 @@ const STORAGE_KEYS = { } as const; // ─── Config ────────────────────────────────────────────────────────────────── -const BASE_URL = 'https://b4ae-202-8-116-13.ngrok-free.app'; // TODO: replace with your actual base URL +const BASE_URL = 'https://sg-delivery-api.convexsol.co'; // TODO: replace with your actual base URL // ─── Token Helpers ─────────────────────────────────────────────────────────── export const tokenManager = { diff --git a/app/services/locationServices.ts b/app/services/locationServices.ts index e92c780..1767247 100644 --- a/app/services/locationServices.ts +++ b/app/services/locationServices.ts @@ -11,9 +11,19 @@ export interface LatLng { longitude: number; } +export interface GeocodeResult { + address: string; + city: string; + state: string; + postalCode: string; +} + export interface LocationResult { coords: LatLng; address: string; + city: string; + state: string; + postalCode: string; } // --------------------------------------------------------------------------- @@ -148,7 +158,7 @@ export const getCurrentPosition = async (): Promise => { * Reverse-geocode a lat/lng pair into a human-readable address string * using the Google Maps Geocoding API. */ -export const reverseGeocode = async (coords: LatLng): Promise => { +export const reverseGeocode = async (coords: LatLng): Promise => { try { const url = `https://maps.googleapis.com/maps/api/geocode/json` + @@ -159,8 +169,31 @@ export const reverseGeocode = async (coords: LatLng): Promise => { const data = await response.json(); if (data.status === 'OK' && data.results.length > 0) { - // Return the most specific formatted address. - return data.results[0].formatted_address; + // Extract city, state, postalCode + let city = ''; + let state = ''; + let postalCode = ''; + const result = data.results[0]; + + result.address_components.forEach((component: any) => { + if (component.types.includes('locality') || component.types.includes('sublocality')) { + city = city || component.long_name; + } + if (component.types.includes('administrative_area_level_1')) { + state = component.long_name; + } + if (component.types.includes('postal_code')) { + postalCode = component.long_name; + } + }); + + // Return the most specific formatted address and extracted details + return { + address: result.formatted_address, + city, + state, + postalCode, + }; } console.warn('Google Maps Geocoding failed:', data.status, data.error_message || ''); @@ -174,10 +207,15 @@ export const reverseGeocode = async (coords: LatLng): Promise => { }); const osmData = await osmResponse.json(); if (osmData && osmData.display_name) { - return osmData.display_name; + return { + address: osmData.display_name, + city: osmData.address?.city || osmData.address?.town || osmData.address?.village || osmData.address?.county || '', + state: osmData.address?.state || '', + postalCode: osmData.address?.postcode || '', + }; } - return 'Address not found'; + return { address: 'Address not found', city: '', state: '', postalCode: '' }; } catch (error) { console.log('reverseGeocode error', error); try { @@ -190,12 +228,17 @@ export const reverseGeocode = async (coords: LatLng): Promise => { }); const osmData = await osmResponse.json(); if (osmData && osmData.display_name) { - return osmData.display_name; + return { + address: osmData.display_name, + city: osmData.address?.city || osmData.address?.town || osmData.address?.village || osmData.address?.county || '', + state: osmData.address?.state || '', + postalCode: osmData.address?.postcode || '', + }; } } catch (osmError) { console.log('OSM fallback error', osmError); } - return 'Unable to fetch address'; + return { address: 'Unable to fetch address', city: '', state: '', postalCode: '' }; } }; @@ -220,12 +263,18 @@ export const getCurrentLocationWithAddress = } const coords = await getCurrentPosition(); - const address = await reverseGeocode(coords); + const geocodeResult = await reverseGeocode(coords); console.log('coords', coords); - console.log('address', address); + console.log('address', geocodeResult.address); - return { coords, address }; + return { + coords, + address: geocodeResult.address, + city: geocodeResult.city, + state: geocodeResult.state, + postalCode: geocodeResult.postalCode + }; }; // --------------------------------------------------------------------------- diff --git a/app/services/socketService.ts b/app/services/socketService.ts index 002a9ee..2899794 100644 --- a/app/services/socketService.ts +++ b/app/services/socketService.ts @@ -1,6 +1,6 @@ import { io, Socket } from 'socket.io-client'; -const BASE_URL = 'https://b4ae-202-8-116-13.ngrok-free.app'; +const BASE_URL = 'https://sg-delivery-api.convexsol.co'; const SOCKET_URL = `${BASE_URL}/tracking`; export interface DriverLocationUpdate { diff --git a/app/store/commonreducers/customerProfile/thunk.ts b/app/store/commonreducers/customerProfile/thunk.ts index 6e0b187..10e6c04 100644 --- a/app/store/commonreducers/customerProfile/thunk.ts +++ b/app/store/commonreducers/customerProfile/thunk.ts @@ -7,7 +7,7 @@ export const fetchCustomerDetails = createAsyncThunk( async (_, { rejectWithValue }) => { try { const response = await getCustomerDetails(); - console.log(response); + // console.log(response); return response; } catch (error: any) { console.log(error); diff --git a/app/utils/helper.ts b/app/utils/helper.ts index 91e7659..bd112f1 100644 --- a/app/utils/helper.ts +++ b/app/utils/helper.ts @@ -1,5 +1,5 @@ export const getFullUrl = (url?: string) => { - const BASE_URL = 'https://b4ae-202-8-116-13.ngrok-free.app'; + const BASE_URL = 'https://sg-delivery-api.convexsol.co'; if (!url) return ''; return url.startsWith('/') ? `${BASE_URL}${url}` : url; }; @@ -29,3 +29,16 @@ export const formatDate = (dateString?: string) => { year: 'numeric', }); }; + +export const getCategoryEmoji = (name: string): string => { + const n = name.toLowerCase(); + if (n.includes('food')) return 'πŸ”'; + if (n.includes('grocer')) return 'πŸ›’'; + if (n.includes('pharma') || n.includes('medic')) return 'πŸ’Š'; + if (n.includes('meat')) return 'πŸ₯©'; + if (n.includes('flower')) return 'πŸ’'; + if (n.includes('electronic')) return 'πŸ“±'; + if (n.includes('cloth') || n.includes('fashion')) return 'πŸ‘—'; + if (n.includes('bakery') || n.includes('cake')) return 'πŸŽ‚'; + return 'πŸ“¦'; +}; \ No newline at end of file diff --git a/customer_tracking_guide.md b/customer_tracking_guide.md new file mode 100644 index 0000000..d4de015 --- /dev/null +++ b/customer_tracking_guide.md @@ -0,0 +1,278 @@ +# React Native Customer Tracking Integration Guide + +This guide outlines how to connect and implement live driver location tracking in your React Native Customer App. + +--- + +## πŸ“¦ Dependencies + +Install the required packages in your React Native project: + +```bash +npm install socket.io-client react-native-maps +``` + +--- + +## πŸ”Œ 1. Tracking Socket Service (`trackingSocket.ts`) + +Create a socket helper to handle the namespace connection (`/tracking`) and events: + +```typescript +import { io, Socket } from "socket.io-client"; + +const SOCKET_URL = "http://:8000/tracking"; + +export interface DriverLocationUpdate { + orderId: string; + latitude: number; + longitude: number; + heading?: number; + speedKph?: number; + recordedAt: string; +} + +export interface TrackingStatusUpdate { + orderId: string; + status: "OUT_FOR_DELIVERY" | "DELIVERED"; + timestamp: string; +} + +class TrackingSocketService { + private socket: Socket | null = null; + + connect(token: string): Socket { + if (this.socket?.connected) { + return this.socket; + } + + this.socket = io(SOCKET_URL, { + auth: { token }, + transports: ["websocket"], + reconnection: true, + }); + + this.socket.on("connect", () => { + console.log("Connected to Tracking WebSocket Namespace"); + }); + + this.socket.on("connect_error", (error) => { + console.error("Socket Connection Error:", error); + }); + + return this.socket; + } + + joinOrderTracking(orderId: string) { + this.socket?.emit("join_order_tracking", { orderId }); + } + + leaveOrderTracking(orderId: string) { + this.socket?.emit("leave_order_tracking", { orderId }); + } + + onDriverLocationUpdate(callback: (data: DriverLocationUpdate) => void) { + this.socket?.on("driver_location_update", callback); + } + + onOrderStatusUpdate(callback: (data: TrackingStatusUpdate) => void) { + this.socket?.on("order_tracking_status", callback); + } + + disconnect(orderId: string) { + if (this.socket) { + this.leaveOrderTracking(orderId); + this.socket.off("driver_location_update"); + this.socket.off("order_tracking_status"); + this.socket.disconnect(); + this.socket = null; + } + } +} + +export const trackingSocketService = new TrackingSocketService(); +``` + +--- + +## πŸͺ 2. Customer Tracking Hook (`useOrderTracking.ts`) + +Manage room state subscription, events listening, and automatic teardown on component unmount: + +```typescript +import { useEffect, useState } from "react"; +import { trackingSocketService, DriverLocationUpdate } from "./trackingSocket"; + +export function useOrderTracking(orderId: string, userJwtToken: string) { + const [driverLocation, setDriverLocation] = + useState(null); + const [orderStatus, setOrderStatus] = useState(null); + + useEffect(() => { + if (!orderId || !userJwtToken) return; + + const socket = trackingSocketService.connect(userJwtToken); + + trackingSocketService.joinOrderTracking(orderId); + + trackingSocketService.onDriverLocationUpdate((data) => { + if (data.orderId === orderId) { + setDriverLocation(data); + } + }); + + trackingSocketService.onOrderStatusUpdate((data) => { + if (data.orderId === orderId) { + setOrderStatus(data.status); + } + }); + + return () => { + trackingSocketService.disconnect(orderId); + }; + }, [orderId, userJwtToken]); + + return { driverLocation, orderStatus }; +} +``` + +--- + +## πŸ—ΊοΈ 3. Tracking Screen Component + +Render Google Maps with the destination address, live driver position, and auto-camera alignment: + +```tsx +import React, { useRef, useEffect } from "react"; +import { StyleSheet, View, Text } from "react-native"; +import MapView, { Marker, PROVIDER_GOOGLE } from "react-native-maps"; +import { useOrderTracking } from "./useOrderTracking"; + +interface TrackingScreenProps { + orderId: string; + token: string; + customerDropoff: { latitude: number; longitude: number }; +} + +export default function OrderTrackingScreen({ + orderId, + token, + customerDropoff, +}: TrackingScreenProps) { + const mapRef = useRef(null); + const { driverLocation, orderStatus } = useOrderTracking(orderId, token); + + useEffect(() => { + if (driverLocation && mapRef.current) { + mapRef.current.animateToRegion( + { + latitude: driverLocation.latitude, + longitude: driverLocation.longitude, + latitudeDelta: 0.01, + longitudeDelta: 0.01, + }, + 1000, + ); + } + }, [driverLocation]); + + return ( + + + {/* Customer dropoff point */} + + + {/* Live Driver */} + {driverLocation && ( + + + πŸ›΅ + + + )} + + + + + Status:{" "} + {orderStatus === "OUT_FOR_DELIVERY" + ? "Out for Delivery πŸ›΅" + : orderStatus || "Preparing"} + + {driverLocation?.speedKph !== undefined && ( + + Driver speed: {Math.round(driverLocation.speedKph)} km/h + + )} + + + ); +} + +const styles = StyleSheet.create({ + container: { + ...StyleSheet.absoluteFillObject, + justifyContent: "flex-end", + alignItems: "center", + }, + map: { + ...StyleSheet.absoluteFillObject, + }, + driverMarker: { + backgroundColor: "#FFF", + padding: 6, + borderRadius: 20, + borderWidth: 2, + borderColor: "#FF7F00", + elevation: 4, + }, + driverEmoji: { + fontSize: 20, + }, + statusContainer: { + position: "absolute", + bottom: 40, + backgroundColor: "white", + padding: 16, + borderRadius: 12, + width: "90%", + elevation: 5, + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.25, + shadowRadius: 3.84, + }, + statusTitle: { + fontSize: 16, + fontWeight: "bold", + color: "#333", + }, + statusSubtitle: { + fontSize: 14, + color: "#666", + marginTop: 4, + }, +}); +``` diff --git a/payment_integration_guide.md b/payment_integration_guide.md new file mode 100644 index 0000000..0bb4d67 --- /dev/null +++ b/payment_integration_guide.md @@ -0,0 +1,131 @@ +# Frontend Payment SDK & API Integration Guide + +This guide details how to integrate your client-side application (Web/React/React Native) with the backend Payment & Wallet module using Stripe Elements and Razorpay Checkout SDKs. + +--- + +## 2. Razorpay Integration (Web & Mobile) + +Razorpay uses **Orders API** where the backend pre-creates a transaction order, and the frontend opens a secure Checkout Widget overlay. + +### SDK Installation + +```html + + +``` + +```bash +# For Mobile (React Native) +npm install react-native-razorpay +``` + +### Complete Razorpay Checkout Flow (Web) + +```typescript +declare const Razorpay: any; + +export const RazorpayCheckout = ({ orderId, totalAmount, userProfile }) => { + const handlePayment = async () => { + // Step 1: Initialize Payment Session on backend to get Razorpay Order ID + const sessionResponse = await fetch('/payments/create-session', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ orderId, paymentMethod: 'UPI' }), // OR 'CARD' + }); + + const { gatewayOrderId, provider } = await sessionResponse.json(); + + // Step 2: Set up checkout configuration options + const options = { + key: process.env.REACT_APP_RAZORPAY_KEY_ID, // Enter public key + amount: totalAmount * 100, // Amount in paise + currency: 'INR', + name: 'Delivery Platform', + description: `Payment for Order #${orderId}`, + order_id: gatewayOrderId, // The Order ID fetched from backend + prefill: { + name: userProfile.name, + email: userProfile.email, + contact: userProfile.phone, + }, + theme: { color: '#3399cc' }, + // Step 3: Signature validation callback handler + handler: async (response: { + razorpay_payment_id: string; + razorpay_order_id: string; + razorpay_signature: string; + }) => { + // Step 4: Verify signature cryptographically on the backend + const verifyResponse = await fetch('/payments/process', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + gatewayOrderId: response.razorpay_order_id, + gatewayPaymentId: response.razorpay_payment_id, + gatewaySignature: response.razorpay_signature, + }), + }); + + const confirmation = await verifyResponse.json(); + if (confirmation.success) { + alert('Order Placed and Driver Dispatched successfully!'); + } + }, + modal: { + ondismiss: function () { + console.log('Payment checkout dialog closed by customer.'); + }, + }, + }; + + const rzp = new Razorpay(options); + rzp.open(); + }; + + return ; +}; +``` + +--- + +## 3. Wallet Top-Up Flow (Backend-Only Deduction) + +Wallet checkouts and top-ups run completely inside backend transactions without client-side redirects. + +```typescript +export const WalletPayment = ({ orderId, walletBalance, totalAmount }) => { + const handleWalletPay = async () => { + if (walletBalance < totalAmount) { + alert('Insufficient wallet balance. Please top up.'); + return; + } + + // Initialize session for WALLET (processed fully by backend transaction) + const response = await fetch('/payments/create-session', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ orderId, paymentMethod: 'WALLET' }), + }); + + const result = await response.json(); + + // For wallet, session creation directly attempts to deduct balance + const verifyResponse = await fetch('/payments/process', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + gatewayOrderId: result.paymentReference, // Virtual reference for wallet + gatewayPaymentId: `wallet_txn_${Date.now()}`, + }), + }); + + const confirmation = await verifyResponse.json(); + if (confirmation.success) { + alert('Balance deducted and order confirmed!'); + } + }; + + return ; +}; +```