feat: implement order status tracking component and wallet management infrastructure
This commit is contained in:
parent
8c443d76eb
commit
eecbf9484c
@ -8,3 +8,4 @@ export * from './customerDetailsApi';
|
||||
export * from './orderApi';
|
||||
export * from './offerApi';
|
||||
export * from './reviewApi';
|
||||
export * from './walletApi';
|
||||
15
app/api/walletApi.ts
Normal file
15
app/api/walletApi.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { WalletTopUpPaymentSessionResponse } from "@interfaces";
|
||||
import { apiClient } from "@services";
|
||||
|
||||
export interface TopupPayload {
|
||||
amount: number,
|
||||
paymentMethod: string
|
||||
}
|
||||
|
||||
export const initializeWalletTopUpApi = async (
|
||||
payload: TopupPayload): Promise<WalletTopUpPaymentSessionResponse> => {
|
||||
return await apiClient.post<WalletTopUpPaymentSessionResponse>(
|
||||
'/wallets/topup',
|
||||
payload
|
||||
);
|
||||
};
|
||||
@ -207,8 +207,7 @@ export const OrderStatusTimeline: React.FC<OrderStatusTimelineProps> = ({
|
||||
];
|
||||
|
||||
const showDriver = [
|
||||
'OUT_FOR_DELIVERY',
|
||||
'DELIVERED',
|
||||
'OUT_FOR_DELIVERY'
|
||||
].includes(currentStatus);
|
||||
const currentIndex = STATUS_FLOW.indexOf(currentStatus);
|
||||
|
||||
|
||||
102
app/features/screens/walletScreen/hooks/useTopUp.ts
Normal file
102
app/features/screens/walletScreen/hooks/useTopUp.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Alert } from 'react-native';
|
||||
import { useStripe } from '@stripe/stripe-react-native';
|
||||
import { PlaceOrderResponse, WalletTopUpPaymentSessionResponse } from '@interfaces';
|
||||
import { verifyPaymentApi } from '@api';
|
||||
|
||||
/**
|
||||
* Custom hook that encapsulates the entire Stripe card payment flow:
|
||||
* 1. Extract session info from the place-order response
|
||||
* 2. Initialize the Stripe Payment Sheet
|
||||
* 3. Present the Payment Sheet to the user
|
||||
* 4. Verify the payment on the backend via /payments/process
|
||||
*
|
||||
* Returns:
|
||||
* - processCardPayment: async function to run the full flow
|
||||
* - isPaymentProcessing: loading flag for UI
|
||||
*/
|
||||
export const useTopUp = () => {
|
||||
const { initPaymentSheet, presentPaymentSheet } = useStripe();
|
||||
const [isPaymentProcessing, setIsPaymentProcessing] = useState(false);
|
||||
|
||||
/**
|
||||
* Runs the complete Stripe card payment flow.
|
||||
* @param response – the PlaceOrderResponse from the backend (contains checkoutSessions)
|
||||
* @returns `true` if payment was verified successfully, `false` otherwise
|
||||
*/
|
||||
const topUpCardPayment = useCallback(
|
||||
async (response: WalletTopUpPaymentSessionResponse): Promise<boolean> => {
|
||||
try {
|
||||
setIsPaymentProcessing(true);
|
||||
|
||||
// ── 1. Extract session details ─────────────────────────────────────
|
||||
// const session = response.checkoutSessions?.[0];
|
||||
// console.log('session', session);
|
||||
const clientSecret = response?.gatewayToken;
|
||||
// console.log('clientSecret', clientSecret);
|
||||
const gatewayOrderId = response?.gatewayOrderId;
|
||||
// console.log('gatewayOrderId', gatewayOrderId);
|
||||
|
||||
if (!clientSecret) {
|
||||
Alert.alert('Error', 'Payment session token not found.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── 2. Initialize Payment Sheet ────────────────────────────────────
|
||||
const { error: initError } = await initPaymentSheet({
|
||||
paymentIntentClientSecret: clientSecret,
|
||||
merchantDisplayName: 'SG Delivery',
|
||||
});
|
||||
|
||||
if (initError) {
|
||||
Alert.alert('Error', `Stripe init failed: ${initError.message}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── 3. Present Payment Sheet ───────────────────────────────────────
|
||||
const { error: presentError } = await presentPaymentSheet();
|
||||
|
||||
if (presentError) {
|
||||
if (presentError.code === 'Canceled') {
|
||||
Alert.alert(
|
||||
'Payment Cancelled',
|
||||
'You cancelled the payment process.',
|
||||
);
|
||||
} else {
|
||||
console.log(presentError.message);
|
||||
Alert.alert('Payment Error', presentError.message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── 4. Verify payment on backend ───────────────────────────────────
|
||||
const verifyRes = await verifyPaymentApi({
|
||||
gatewayOrderId: gatewayOrderId || '',
|
||||
gatewayPaymentId: gatewayOrderId || '',
|
||||
gatewaySignature: 'stripe_signature_verified',
|
||||
});
|
||||
|
||||
if (verifyRes.success) {
|
||||
return true;
|
||||
} else {
|
||||
Alert.alert(
|
||||
'Verification Failed',
|
||||
verifyRes.message || 'Unable to confirm payment.',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
} catch (error: any) {
|
||||
Alert.alert(
|
||||
'Payment Error',
|
||||
error?.message || 'Something went wrong during payment.',
|
||||
);
|
||||
return false;
|
||||
} finally {
|
||||
setIsPaymentProcessing(false);
|
||||
}
|
||||
},
|
||||
[initPaymentSheet, presentPaymentSheet],
|
||||
);
|
||||
|
||||
return { topUpCardPayment, isPaymentProcessing };
|
||||
};
|
||||
@ -1,17 +1,19 @@
|
||||
import { WalletTransaction } from '@interfaces';
|
||||
import { WalletTopUpPaymentSessionResponse, WalletTransaction } from '@interfaces';
|
||||
import { createReducer } from '@reduxjs/toolkit';
|
||||
import { getAllWalletTransactions } from './thunk';
|
||||
import { getAllWalletTransactions, initializeWalletTopUpThunk } from './thunk';
|
||||
|
||||
export interface walletState {
|
||||
walletTransactions: WalletTransaction[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
walletTopUpPaymentSessionResponse: WalletTopUpPaymentSessionResponse | null;
|
||||
}
|
||||
|
||||
const initialState: walletState = {
|
||||
walletTransactions: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
walletTopUpPaymentSessionResponse: null,
|
||||
};
|
||||
|
||||
export const walletReducer = createReducer(initialState, builder => {
|
||||
@ -27,4 +29,16 @@ export const walletReducer = createReducer(initialState, builder => {
|
||||
state.loading = false;
|
||||
state.error = action.payload as string | null;
|
||||
});
|
||||
builder.addCase(initializeWalletTopUpThunk.pending, state => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(initializeWalletTopUpThunk.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.walletTopUpPaymentSessionResponse = action.payload;
|
||||
});
|
||||
builder.addCase(initializeWalletTopUpThunk.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload as string | null;
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { getWalletTransactions } from '@api';
|
||||
import { getWalletTransactions, initializeWalletTopUpApi, TopupPayload } from '@api';
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
|
||||
export const getAllWalletTransactions = createAsyncThunk(
|
||||
@ -12,3 +12,15 @@ export const getAllWalletTransactions = createAsyncThunk(
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const initializeWalletTopUpThunk = createAsyncThunk(
|
||||
'wallet/initializeWalletTopUp',
|
||||
async (payload: TopupPayload, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await initializeWalletTopUpApi(payload);
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.response.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@ -125,4 +125,97 @@ export const getStyles = (theme: any) =>
|
||||
color: '#888',
|
||||
fontSize: 16,
|
||||
},
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
|
||||
modalBackdrop: {
|
||||
...StyleSheet.absoluteFill,
|
||||
backgroundColor: 'rgba(0,0,0,0.4)',
|
||||
},
|
||||
|
||||
modalCard: {
|
||||
backgroundColor: '#FFF',
|
||||
borderTopLeftRadius: 24,
|
||||
borderTopRightRadius: 24,
|
||||
padding: 24,
|
||||
paddingBottom: 32,
|
||||
},
|
||||
|
||||
modalTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
color: '#222',
|
||||
},
|
||||
|
||||
modalSubtitle: {
|
||||
fontSize: 13,
|
||||
color: '#888',
|
||||
marginTop: 4,
|
||||
marginBottom: 20,
|
||||
},
|
||||
|
||||
amountInputWrapper: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: '#E0E0E0',
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 16,
|
||||
height: 56,
|
||||
},
|
||||
|
||||
rupeeSymbol: {
|
||||
fontSize: 22,
|
||||
fontWeight: '700',
|
||||
color: '#222',
|
||||
marginRight: 8,
|
||||
},
|
||||
|
||||
amountInput: {
|
||||
flex: 1,
|
||||
fontSize: 22,
|
||||
fontWeight: '700',
|
||||
color: '#222',
|
||||
padding: 0,
|
||||
},
|
||||
|
||||
modalError: {
|
||||
color: '#F04438',
|
||||
fontSize: 13,
|
||||
marginTop: 8,
|
||||
},
|
||||
|
||||
topUpButton: {
|
||||
marginTop: 24,
|
||||
backgroundColor: '#2F80ED',
|
||||
height: 52,
|
||||
borderRadius: 12,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
|
||||
topUpButtonDisabled: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
|
||||
topUpButtonText: {
|
||||
color: '#FFF',
|
||||
fontWeight: '700',
|
||||
fontSize: 16,
|
||||
},
|
||||
|
||||
modalCancelButton: {
|
||||
marginTop: 12,
|
||||
height: 48,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
|
||||
modalCancelText: {
|
||||
color: '#888',
|
||||
fontWeight: '600',
|
||||
fontSize: 15,
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,10 +1,15 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
SafeAreaView,
|
||||
FlatList,
|
||||
TouchableOpacity,
|
||||
Modal,
|
||||
TextInput,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
Pressable,
|
||||
} from 'react-native';
|
||||
// import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import { Header } from '@components';
|
||||
@ -12,42 +17,12 @@ import { useAppTheme } from '@theme';
|
||||
import { getStyles } from './walletScreen.styles';
|
||||
import { WalletTransaction } from '@interfaces';
|
||||
import { useAppDispatch, useAppSelector } from '@store';
|
||||
import { getAllWalletTransactions } from './thunk';
|
||||
import { getAllWalletTransactions, initializeWalletTopUpThunk } from './thunk';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import { getWalletBalanceThunk } from '../accountScreen';
|
||||
|
||||
// const balance = 17496.05;
|
||||
|
||||
// const transactions: WalletTransaction[] = [
|
||||
// {
|
||||
// id: '1',
|
||||
// walletId: '',
|
||||
// transactionReference: 'TXN-1784543790637-2387',
|
||||
// direction: 'OUT',
|
||||
// amount: '1408.95',
|
||||
// balanceBefore: '18905',
|
||||
// balanceAfter: '17496.05',
|
||||
// type: 'PAYMENT',
|
||||
// referenceId: null,
|
||||
// description: null,
|
||||
// idempotencyKey: null,
|
||||
// createdAt: '2026-07-20T10:36:30.639Z',
|
||||
// },
|
||||
// {
|
||||
// id: '2',
|
||||
// walletId: '',
|
||||
// transactionReference: 'TXN-1784199810512-8502',
|
||||
// direction: 'OUT',
|
||||
// amount: '1095',
|
||||
// balanceBefore: '20000',
|
||||
// balanceAfter: '18905',
|
||||
// type: 'PAYMENT',
|
||||
// referenceId: null,
|
||||
// description: null,
|
||||
// idempotencyKey: null,
|
||||
// createdAt: '2026-07-16T11:03:30.515Z',
|
||||
// },
|
||||
// ];
|
||||
import { useStripePayment } from '../checkoutPaymentScreen/hooks/useStripePayment';
|
||||
import { useTopUp } from './hooks/useTopUp';
|
||||
// import { addMoneyThunk } from './thunk'; // wire this up to your actual top-up API
|
||||
|
||||
const WalletScreen = () => {
|
||||
const { colors } = useAppTheme();
|
||||
@ -57,6 +32,12 @@ const WalletScreen = () => {
|
||||
const { walletTransactions } = useAppSelector(state => state.wallet);
|
||||
const { wallet } = useAppSelector(state => state.account);
|
||||
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [amount, setAmount] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const { topUpCardPayment, isPaymentProcessing } = useTopUp();
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
dispatch(getAllWalletTransactions());
|
||||
@ -64,6 +45,50 @@ const WalletScreen = () => {
|
||||
}, []),
|
||||
);
|
||||
|
||||
const openModal = () => {
|
||||
setAmount('');
|
||||
setError('');
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
if (isSubmitting) return;
|
||||
setIsModalVisible(false);
|
||||
};
|
||||
|
||||
const handleAmountChange = (text: string) => {
|
||||
// allow only digits and a single decimal point
|
||||
const sanitized = text.replace(/[^0-9.]/g, '');
|
||||
setAmount(sanitized);
|
||||
if (error) setError('');
|
||||
};
|
||||
|
||||
const handleTopUp = async () => {
|
||||
const numericAmount = parseFloat(amount);
|
||||
|
||||
if (!amount || isNaN(numericAmount) || numericAmount <= 0) {
|
||||
setError('Please enter a valid amount');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
const payload = {
|
||||
amount: numericAmount,
|
||||
paymentMethod: 'CARD'
|
||||
}
|
||||
|
||||
const response = await dispatch(initializeWalletTopUpThunk(payload)).unwrap();
|
||||
const success = await topUpCardPayment(response);
|
||||
setIsSubmitting(false);
|
||||
setIsModalVisible(false);
|
||||
setAmount('');
|
||||
} catch (e) {
|
||||
setIsSubmitting(false);
|
||||
setError('Something went wrong. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const renderItem = ({ item }: { item: WalletTransaction }) => {
|
||||
const isCredit = item.direction === 'IN';
|
||||
|
||||
@ -113,7 +138,7 @@ const WalletScreen = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<KeyboardAvoidingView style={styles.container}>
|
||||
<Header title="Wallet" />
|
||||
|
||||
<View style={styles.balanceCard}>
|
||||
@ -121,7 +146,7 @@ const WalletScreen = () => {
|
||||
|
||||
<Text style={styles.balance}>₹{wallet?.balance.toFixed(2)}</Text>
|
||||
|
||||
<TouchableOpacity style={styles.addMoneyButton}>
|
||||
<TouchableOpacity style={styles.addMoneyButton} onPress={openModal}>
|
||||
{/* <Icon name="plus" color="#FFF" size={20} /> */}
|
||||
<Text style={styles.addMoneyText}>Add Money</Text>
|
||||
</TouchableOpacity>
|
||||
@ -142,7 +167,64 @@ const WalletScreen = () => {
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
|
||||
<Modal
|
||||
visible={isModalVisible}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={closeModal}
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
style={styles.modalOverlay}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
>
|
||||
<Pressable style={styles.modalBackdrop} onPress={closeModal} />
|
||||
|
||||
<View style={styles.modalCard}>
|
||||
<Text style={styles.modalTitle}>Add Money</Text>
|
||||
<Text style={styles.modalSubtitle}>
|
||||
Enter an amount to top up your wallet
|
||||
</Text>
|
||||
|
||||
<View style={styles.amountInputWrapper}>
|
||||
<Text style={styles.rupeeSymbol}>₹</Text>
|
||||
<TextInput
|
||||
style={styles.amountInput}
|
||||
keyboardType="decimal-pad"
|
||||
placeholder="0.00"
|
||||
placeholderTextColor="#B0B0B0"
|
||||
value={amount}
|
||||
onChangeText={handleAmountChange}
|
||||
autoFocus
|
||||
/>
|
||||
</View>
|
||||
|
||||
{error ? <Text style={styles.modalError}>{error}</Text> : null}
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.topUpButton,
|
||||
isSubmitting && styles.topUpButtonDisabled,
|
||||
]}
|
||||
onPress={handleTopUp}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<Text style={styles.topUpButtonText}>
|
||||
{isSubmitting ? 'Processing...' : 'Add Top Up'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.modalCancelButton}
|
||||
onPress={closeModal}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<Text style={styles.modalCancelText}>Cancel</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</Modal>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -22,3 +22,12 @@ export type TransactionType =
|
||||
| 'WITHDRAWAL'
|
||||
| 'TRANSFER'
|
||||
| 'ADJUSTMENT';
|
||||
|
||||
export interface WalletTopUpPaymentSessionResponse {
|
||||
message: string;
|
||||
paymentId: string;
|
||||
paymentReference: string;
|
||||
gatewayOrderId: string;
|
||||
gatewayToken: string;
|
||||
amount: number;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user