feat: implement core customer profile screens, real-time order tracking services, and payment integration modules

This commit is contained in:
Atanu Das 2026-07-22 13:09:40 +05:30
parent 40b21a4446
commit da1feb91e4
17 changed files with 537 additions and 41 deletions

1
.env
View File

@ -1,3 +1,4 @@
STRIPE_SECRET_KEY=sk_test_51Ox04DSHlFQYe8R5HXy6nj0eQqtqAP4ynF7ODFg71ork78B38MPsDV3gQEo2EYaFL9OG75L8tG7bKxptsmVeONrS00ji44eUIl STRIPE_SECRET_KEY=sk_test_51Ox04DSHlFQYe8R5HXy6nj0eQqtqAP4ynF7ODFg71ork78B38MPsDV3gQEo2EYaFL9OG75L8tG7bKxptsmVeONrS00ji44eUIl
STRIPE_PUBLISHABLE_KEY=pk_test_51Ox04DSHlFQYe8R5Cvm8i6n99QynUNj7WQzACB89PImnt0X8Z54SBFM22ghSNHYFo7OgVcyba9QhhyrrdRqRPJpF00Us8XJv7f
RAZORPAY_KEY_ID=rzp_test_TCxbW8AxcXgMCj RAZORPAY_KEY_ID=rzp_test_TCxbW8AxcXgMCj
RAZORPAY_KEY_SECRET=rLlM87yFehhsVNSqjCE2qRz5 RAZORPAY_KEY_SECRET=rLlM87yFehhsVNSqjCE2qRz5

View File

@ -206,8 +206,6 @@ export const OrderStatusTimeline: React.FC<OrderStatusTimelineProps> = ({
]; ];
const showDriver = [ const showDriver = [
'PREPARING',
'READY_FOR_PICKUP',
'OUT_FOR_DELIVERY', 'OUT_FOR_DELIVERY',
'DELIVERED', 'DELIVERED',
].includes(currentStatus); ].includes(currentStatus);

View File

@ -32,7 +32,7 @@ export const CheckoutAddressScreen: React.FC = () => {
const navigation = useNavigation<CheckoutAddressNavProp>(); const navigation = useNavigation<CheckoutAddressNavProp>();
const { customerDetails } = useAppSelector(state => state.customerProfile); const { customerDetails } = useAppSelector(state => state.customerProfile);
const addresses: CustomerAddress[] = customerDetails?.addresses ?? []; const addresses: CustomerAddress[] = useMemo(() => customerDetails?.addresses ?? [], [customerDetails]);
const defaultAddressId = useMemo(() => { const defaultAddressId = useMemo(() => {
const def = addresses.find(a => a.isDefault); const def = addresses.find(a => a.isDefault);

View File

@ -109,7 +109,7 @@ export const CheckoutPaymentScreen: React.FC = () => {
} }
} else { } else {
// Non-card methods (COD, Wallet) navigate directly // Non-card methods (COD, Wallet) navigate directly
navigation.navigate('OrderConfirmedScreen', { navigation.replace('OrderConfirmedScreen', {
orderId: orderId:
response.orders[0]?.orderNumber || response.orders[0]?.orderNumber ||
'ORD-' + Math.floor(Math.random() * 900000 + 100000), 'ORD-' + Math.floor(Math.random() * 900000 + 100000),

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { import {
View, View,
Text, Text,
@ -12,8 +12,7 @@ import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './completeProfileScreen.styles'; import { getStyles } from './completeProfileScreen.styles';
import { CustomInput, PrimaryButton } from '@components'; import { CustomInput, PrimaryButton } from '@components';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { AuthStackParamList } from '../../../navigation/authStack'; import { useAppDispatch, useAppSelector } from '@store';
import { useAppDispatch } from '@store';
import { saveProfileData } from './reducer'; import { saveProfileData } from './reducer';
import { OnboardingStackParamList } from '@navigation/onboardingStack'; import { OnboardingStackParamList } from '@navigation/onboardingStack';
@ -44,6 +43,14 @@ export const CompleteProfileScreen: React.FC = () => {
const [selectedLabel, setSelectedLabel] = useState('Home'); 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 = () => { const handleSave = () => {
dispatch( dispatch(
saveProfileData({ saveProfileData({

View File

@ -30,6 +30,7 @@ import {
useAppSelector, useAppSelector,
} from '@store'; } from '@store';
import { getAllCategoriesThunk, getAllProductsThunk } from './thunk'; import { getAllCategoriesThunk, getAllProductsThunk } from './thunk';
import { getCategoryEmoji } from '@utils';
type NavProp = CompositeNavigationProp< type NavProp = CompositeNavigationProp<
BottomTabNavigationProp<MainTabParamList, 'HomeScreen'>, BottomTabNavigationProp<MainTabParamList, 'HomeScreen'>,
@ -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 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 = [ const PROMOS = [
{ {
@ -90,7 +80,7 @@ export const HomeScreen: React.FC = () => {
const { products, isLoading, categories } = useAppSelector( const { products, isLoading, categories } = useAppSelector(
state => state.home, state => state.home,
); );
const { customerDetails } = useAppSelector(state => state.customerProfile);
const [selectedCategory, setSelectedCategory] = useState('all'); const [selectedCategory, setSelectedCategory] = useState('all');
const [activeBanner, setActiveBanner] = useState(0); const [activeBanner, setActiveBanner] = useState(0);
@ -137,7 +127,7 @@ export const HomeScreen: React.FC = () => {
<Text style={styles.addressLabel}>Deliver to</Text> <Text style={styles.addressLabel}>Deliver to</Text>
<View style={styles.addressRow}> <View style={styles.addressRow}>
<Text style={styles.addressText} numberOfLines={1}> <Text style={styles.addressText} numberOfLines={1}>
Koramangala 4th Block {customerDetails?.addresses[0]?.mapAddress}
</Text> </Text>
<Text style={styles.dropdownArrow}></Text> <Text style={styles.dropdownArrow}></Text>
</View> </View>

View File

@ -32,7 +32,7 @@ export const OrderConfirmedScreen: React.FC = () => {
</View> </View>
</View> </View>
<View style={styles.footer}> <View style={styles.footer}>
<PrimaryButton title="View Tracking" onPress={() => navigation.navigate('OrderTrackingScreen', { orderId })} /> <PrimaryButton title="Done" onPress={() => navigation.replace('MainTabs')} />
</View> </View>
</View> </View>
); );

View File

@ -77,7 +77,7 @@ export const useOtpScreen = () => {
verifyOtp({ phone: mobileNumber, code: otpString, role: 'CUSTOMER' }), verifyOtp({ phone: mobileNumber, code: otpString, role: 'CUSTOMER' }),
).unwrap(); ).unwrap();
navigation.navigate('SetLocationScreen'); // navigation.navigate('SetLocationScreen');
} catch (error) { } catch (error) {
const message = const message =
typeof error === 'string' ? error : 'Invalid OTP. Please try again.'; typeof error === 'string' ? error : 'Invalid OTP. Please try again.';

View File

@ -6,6 +6,9 @@ export const setLocationData = createAction<{
latitude: number; latitude: number;
longitude: number; longitude: number;
mapAddress: string; mapAddress: string;
city: string;
state: string;
postalCode: string;
}>('setLocation/setLocationData'); }>('setLocation/setLocationData');
export const clearLocationData = createAction('setLocation/clearLocationData'); export const clearLocationData = createAction('setLocation/clearLocationData');
@ -16,12 +19,18 @@ export interface SetLocationState {
latitude: number | null; latitude: number | null;
longitude: number | null; longitude: number | null;
mapAddress: string; mapAddress: string;
city: string;
state: string;
postalCode: string;
} }
const initialState: SetLocationState = { const initialState: SetLocationState = {
latitude: null, latitude: null,
longitude: null, longitude: null,
mapAddress: '', mapAddress: '',
city: '',
state: '',
postalCode: '',
}; };
// ─── Reducer ────────────────────────────────────────────────────────────────── // ─── Reducer ──────────────────────────────────────────────────────────────────
@ -32,11 +41,17 @@ const setLocationReducer = createReducer(initialState, builder => {
state.latitude = action.payload.latitude; state.latitude = action.payload.latitude;
state.longitude = action.payload.longitude; state.longitude = action.payload.longitude;
state.mapAddress = action.payload.mapAddress; state.mapAddress = action.payload.mapAddress;
state.city = action.payload.city;
state.state = action.payload.state;
state.postalCode = action.payload.postalCode;
}) })
.addCase(clearLocationData, state => { .addCase(clearLocationData, state => {
state.latitude = null; state.latitude = null;
state.longitude = null; state.longitude = null;
state.mapAddress = ''; state.mapAddress = '';
state.city = '';
state.state = '';
state.postalCode = '';
}); });
}); });

View File

@ -12,7 +12,6 @@ import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './setLocationScreen.styles'; import { getStyles } from './setLocationScreen.styles';
import { PrimaryButton } from '@components'; import { PrimaryButton } from '@components';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { AuthStackParamList } from '../../../navigation/authStack';
import { import {
DEFAULT_LOCATION, DEFAULT_LOCATION,
getCurrentLocationWithAddress, getCurrentLocationWithAddress,
@ -42,6 +41,9 @@ export const SetLocationScreen: React.FC = () => {
const [coords, setCoords] = useState<LatLng>(DEFAULT_LOCATION); const [coords, setCoords] = useState<LatLng>(DEFAULT_LOCATION);
const [address, setAddress] = useState('Fetching your 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); const [loading, setLoading] = useState(true);
// ------------------------------------------------------------------ // ------------------------------------------------------------------
@ -57,6 +59,9 @@ export const SetLocationScreen: React.FC = () => {
if (!isMounted.current) return; if (!isMounted.current) return;
setCoords(result.coords); setCoords(result.coords);
setAddress(result.address); setAddress(result.address);
setCity(result.city || '');
setState(result.state || '');
setPostalCode(result.postalCode || '');
// Animate the map to the user's location // Animate the map to the user's location
mapRef.current?.animateToRegion({ ...result.coords, ...DELTA }, 600); mapRef.current?.animateToRegion({ ...result.coords, ...DELTA }, 600);
@ -97,9 +102,15 @@ export const SetLocationScreen: React.FC = () => {
}; };
setCoords(newCoords); setCoords(newCoords);
setAddress('Fetching address…'); setAddress('Fetching address…');
setCity('');
setState('');
setPostalCode('');
const newAddress = await reverseGeocode(newCoords); const geocodeResult = await reverseGeocode(newCoords);
setAddress(newAddress); setAddress(geocodeResult.address);
setCity(geocodeResult.city);
setState(geocodeResult.state);
setPostalCode(geocodeResult.postalCode);
}, []); }, []);
return ( return (
@ -162,6 +173,9 @@ export const SetLocationScreen: React.FC = () => {
latitude: coords.latitude, latitude: coords.latitude,
longitude: coords.longitude, longitude: coords.longitude,
mapAddress: address, mapAddress: address,
city,
state,
postalCode,
}), }),
); );
navigation.navigate('CompleteProfileScreen'); navigation.navigate('CompleteProfileScreen');

View File

@ -15,7 +15,7 @@ const STORAGE_KEYS = {
} as const; } as const;
// ─── Config ────────────────────────────────────────────────────────────────── // ─── 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 ─────────────────────────────────────────────────────────── // ─── Token Helpers ───────────────────────────────────────────────────────────
export const tokenManager = { export const tokenManager = {

View File

@ -11,9 +11,19 @@ export interface LatLng {
longitude: number; longitude: number;
} }
export interface GeocodeResult {
address: string;
city: string;
state: string;
postalCode: string;
}
export interface LocationResult { export interface LocationResult {
coords: LatLng; coords: LatLng;
address: string; address: string;
city: string;
state: string;
postalCode: string;
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -148,7 +158,7 @@ export const getCurrentPosition = async (): Promise<LatLng> => {
* Reverse-geocode a lat/lng pair into a human-readable address string * Reverse-geocode a lat/lng pair into a human-readable address string
* using the Google Maps Geocoding API. * using the Google Maps Geocoding API.
*/ */
export const reverseGeocode = async (coords: LatLng): Promise<string> => { export const reverseGeocode = async (coords: LatLng): Promise<GeocodeResult> => {
try { try {
const url = const url =
`https://maps.googleapis.com/maps/api/geocode/json` + `https://maps.googleapis.com/maps/api/geocode/json` +
@ -159,8 +169,31 @@ export const reverseGeocode = async (coords: LatLng): Promise<string> => {
const data = await response.json(); const data = await response.json();
if (data.status === 'OK' && data.results.length > 0) { if (data.status === 'OK' && data.results.length > 0) {
// Return the most specific formatted address. // Extract city, state, postalCode
return data.results[0].formatted_address; 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 || ''); console.warn('Google Maps Geocoding failed:', data.status, data.error_message || '');
@ -174,10 +207,15 @@ export const reverseGeocode = async (coords: LatLng): Promise<string> => {
}); });
const osmData = await osmResponse.json(); const osmData = await osmResponse.json();
if (osmData && osmData.display_name) { 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) { } catch (error) {
console.log('reverseGeocode error', error); console.log('reverseGeocode error', error);
try { try {
@ -190,12 +228,17 @@ export const reverseGeocode = async (coords: LatLng): Promise<string> => {
}); });
const osmData = await osmResponse.json(); const osmData = await osmResponse.json();
if (osmData && osmData.display_name) { 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) { } catch (osmError) {
console.log('OSM fallback error', 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 coords = await getCurrentPosition();
const address = await reverseGeocode(coords); const geocodeResult = await reverseGeocode(coords);
console.log('coords', 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
};
}; };
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -1,6 +1,6 @@
import { io, Socket } from 'socket.io-client'; 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`; const SOCKET_URL = `${BASE_URL}/tracking`;
export interface DriverLocationUpdate { export interface DriverLocationUpdate {

View File

@ -7,7 +7,7 @@ export const fetchCustomerDetails = createAsyncThunk<CustomerResponse>(
async (_, { rejectWithValue }) => { async (_, { rejectWithValue }) => {
try { try {
const response = await getCustomerDetails(); const response = await getCustomerDetails();
console.log(response); // console.log(response);
return response; return response;
} catch (error: any) { } catch (error: any) {
console.log(error); console.log(error);

View File

@ -1,5 +1,5 @@
export const getFullUrl = (url?: string) => { 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 ''; if (!url) return '';
return url.startsWith('/') ? `${BASE_URL}${url}` : url; return url.startsWith('/') ? `${BASE_URL}${url}` : url;
}; };
@ -29,3 +29,16 @@ export const formatDate = (dateString?: string) => {
year: 'numeric', 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 '📦';
};

278
customer_tracking_guide.md Normal file
View File

@ -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://<YOUR_BACKEND_IP>: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<DriverLocationUpdate | null>(null);
const [orderStatus, setOrderStatus] = useState<string | null>(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<MapView>(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 (
<View style={styles.container}>
<MapView
ref={mapRef}
provider={PROVIDER_GOOGLE}
style={styles.map}
initialRegion={{
latitude: customerDropoff.latitude,
longitude: customerDropoff.longitude,
latitudeDelta: 0.02,
longitudeDelta: 0.02,
}}
>
{/* Customer dropoff point */}
<Marker
coordinate={customerDropoff}
title="Delivery Location"
pinColor="red"
/>
{/* Live Driver */}
{driverLocation && (
<Marker
coordinate={{
latitude: driverLocation.latitude,
longitude: driverLocation.longitude,
}}
title="Your Delivery Driver"
anchor={{ x: 0.5, y: 0.5 }}
rotation={driverLocation.heading ?? 0}
>
<View style={styles.driverMarker}>
<Text style={styles.driverEmoji}>🛵</Text>
</View>
</Marker>
)}
</MapView>
<View style={styles.statusContainer}>
<Text style={styles.statusTitle}>
Status:{" "}
{orderStatus === "OUT_FOR_DELIVERY"
? "Out for Delivery 🛵"
: orderStatus || "Preparing"}
</Text>
{driverLocation?.speedKph !== undefined && (
<Text style={styles.statusSubtitle}>
Driver speed: {Math.round(driverLocation.speedKph)} km/h
</Text>
)}
</View>
</View>
);
}
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,
},
});
```

View File

@ -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
<!-- For Web: include checkout script in index.html -->
<script src="https://checkout.razorpay.com/v1/checkout.js"></script>
```
```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 <button onClick={handlePayment}>Pay via Razorpay</button>;
};
```
---
## 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 <button onClick={handleWalletPay}>Deduct Wallet Balance</button>;
};
```