From eecbf9484c00a6e2c8c1e0bef54a823e05c525ab Mon Sep 17 00:00:00 2001 From: Atanu Das Date: Thu, 23 Jul 2026 16:06:13 +0530 Subject: [PATCH] feat: implement order status tracking component and wallet management infrastructure --- app/api/index.ts | 1 + app/api/walletApi.ts | 15 ++ .../OrderStatusTimeline.tsx | 3 +- .../screens/walletScreen/hooks/useTopUp.ts | 102 +++++++++++ app/features/screens/walletScreen/reducer.ts | 18 +- app/features/screens/walletScreen/thunk.ts | 14 +- .../walletScreen/walletScreen.styles.ts | 93 ++++++++++ .../screens/walletScreen/walletScreen.tsx | 160 +++++++++++++----- app/interfaces/wallet.ts | 9 + 9 files changed, 371 insertions(+), 44 deletions(-) create mode 100644 app/api/walletApi.ts create mode 100644 app/features/screens/walletScreen/hooks/useTopUp.ts diff --git a/app/api/index.ts b/app/api/index.ts index 54cf825..6a0ecc4 100644 --- a/app/api/index.ts +++ b/app/api/index.ts @@ -8,3 +8,4 @@ export * from './customerDetailsApi'; export * from './orderApi'; export * from './offerApi'; export * from './reviewApi'; +export * from './walletApi'; \ No newline at end of file diff --git a/app/api/walletApi.ts b/app/api/walletApi.ts new file mode 100644 index 0000000..fa1ed94 --- /dev/null +++ b/app/api/walletApi.ts @@ -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 => { + return await apiClient.post( + '/wallets/topup', + payload + ); +}; \ No newline at end of file diff --git a/app/components/OrderStatusTimeline/OrderStatusTimeline.tsx b/app/components/OrderStatusTimeline/OrderStatusTimeline.tsx index ef845d7..6df1e5f 100644 --- a/app/components/OrderStatusTimeline/OrderStatusTimeline.tsx +++ b/app/components/OrderStatusTimeline/OrderStatusTimeline.tsx @@ -207,8 +207,7 @@ export const OrderStatusTimeline: React.FC = ({ ]; const showDriver = [ - 'OUT_FOR_DELIVERY', - 'DELIVERED', + 'OUT_FOR_DELIVERY' ].includes(currentStatus); const currentIndex = STATUS_FLOW.indexOf(currentStatus); diff --git a/app/features/screens/walletScreen/hooks/useTopUp.ts b/app/features/screens/walletScreen/hooks/useTopUp.ts new file mode 100644 index 0000000..0d702b2 --- /dev/null +++ b/app/features/screens/walletScreen/hooks/useTopUp.ts @@ -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 => { + 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 }; +}; diff --git a/app/features/screens/walletScreen/reducer.ts b/app/features/screens/walletScreen/reducer.ts index 426a82c..d48d7ee 100644 --- a/app/features/screens/walletScreen/reducer.ts +++ b/app/features/screens/walletScreen/reducer.ts @@ -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; + }); }); diff --git a/app/features/screens/walletScreen/thunk.ts b/app/features/screens/walletScreen/thunk.ts index 88930d9..05cd16e 100644 --- a/app/features/screens/walletScreen/thunk.ts +++ b/app/features/screens/walletScreen/thunk.ts @@ -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); + } + }, +); diff --git a/app/features/screens/walletScreen/walletScreen.styles.ts b/app/features/screens/walletScreen/walletScreen.styles.ts index 83762c6..1de64e5 100644 --- a/app/features/screens/walletScreen/walletScreen.styles.ts +++ b/app/features/screens/walletScreen/walletScreen.styles.ts @@ -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, + }, }); diff --git a/app/features/screens/walletScreen/walletScreen.tsx b/app/features/screens/walletScreen/walletScreen.tsx index 4d302f7..0ac318b 100644 --- a/app/features/screens/walletScreen/walletScreen.tsx +++ b/app/features/screens/walletScreen/walletScreen.tsx @@ -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 ( - +
@@ -121,7 +146,7 @@ const WalletScreen = () => { ₹{wallet?.balance.toFixed(2)} - + {/* */} Add Money @@ -142,8 +167,65 @@ const WalletScreen = () => { } /> - + + + + + + + Add Money + + Enter an amount to top up your wallet + + + + + + + + {error ? {error} : null} + + + + {isSubmitting ? 'Processing...' : 'Add Top Up'} + + + + + Cancel + + + + + ); }; -export default WalletScreen; +export default WalletScreen; \ No newline at end of file diff --git a/app/interfaces/wallet.ts b/app/interfaces/wallet.ts index 7b4c1ec..cb94e4f 100644 --- a/app/interfaces/wallet.ts +++ b/app/interfaces/wallet.ts @@ -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; +} \ No newline at end of file