feat: implement core customer profile screens, real-time order tracking services, and payment integration modules
This commit is contained in:
parent
40b21a4446
commit
da1feb91e4
1
.env
1
.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
|
||||
@ -206,8 +206,6 @@ export const OrderStatusTimeline: React.FC<OrderStatusTimelineProps> = ({
|
||||
];
|
||||
|
||||
const showDriver = [
|
||||
'PREPARING',
|
||||
'READY_FOR_PICKUP',
|
||||
'OUT_FOR_DELIVERY',
|
||||
'DELIVERED',
|
||||
].includes(currentStatus);
|
||||
|
||||
@ -32,7 +32,7 @@ export const CheckoutAddressScreen: React.FC = () => {
|
||||
const navigation = useNavigation<CheckoutAddressNavProp>();
|
||||
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);
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -30,6 +30,7 @@ import {
|
||||
useAppSelector,
|
||||
} from '@store';
|
||||
import { getAllCategoriesThunk, getAllProductsThunk } from './thunk';
|
||||
import { getCategoryEmoji } from '@utils';
|
||||
|
||||
type NavProp = CompositeNavigationProp<
|
||||
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 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 = () => {
|
||||
<Text style={styles.addressLabel}>Deliver to</Text>
|
||||
<View style={styles.addressRow}>
|
||||
<Text style={styles.addressText} numberOfLines={1}>
|
||||
Koramangala 4th Block
|
||||
{customerDetails?.addresses[0]?.mapAddress}
|
||||
</Text>
|
||||
<Text style={styles.dropdownArrow}>▼</Text>
|
||||
</View>
|
||||
|
||||
@ -32,7 +32,7 @@ export const OrderConfirmedScreen: React.FC = () => {
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.footer}>
|
||||
<PrimaryButton title="View Tracking" onPress={() => navigation.navigate('OrderTrackingScreen', { orderId })} />
|
||||
<PrimaryButton title="Done" onPress={() => navigation.replace('MainTabs')} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
@ -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.';
|
||||
|
||||
@ -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 = '';
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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<LatLng>(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');
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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<LatLng> => {
|
||||
* 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<string> => {
|
||||
export const reverseGeocode = async (coords: LatLng): Promise<GeocodeResult> => {
|
||||
try {
|
||||
const url =
|
||||
`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();
|
||||
|
||||
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<string> => {
|
||||
});
|
||||
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<string> => {
|
||||
});
|
||||
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
|
||||
};
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -7,7 +7,7 @@ export const fetchCustomerDetails = createAsyncThunk<CustomerResponse>(
|
||||
async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await getCustomerDetails();
|
||||
console.log(response);
|
||||
// console.log(response);
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
console.log(error);
|
||||
|
||||
@ -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 '📦';
|
||||
};
|
||||
278
customer_tracking_guide.md
Normal file
278
customer_tracking_guide.md
Normal 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,
|
||||
},
|
||||
});
|
||||
```
|
||||
131
payment_integration_guide.md
Normal file
131
payment_integration_guide.md
Normal 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>;
|
||||
};
|
||||
```
|
||||
Loading…
x
Reference in New Issue
Block a user