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_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
|
||||||
@ -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);
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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({
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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.';
|
||||||
|
|||||||
@ -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 = '';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
@ -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 = {
|
||||||
|
|||||||
@ -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
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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
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