Skip to content

Commit 47c5068

Browse files
committed
add 3-step Checkout screen with address and payment
1 parent 5313ce9 commit 47c5068

1 file changed

Lines changed: 199 additions & 0 deletions

File tree

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import React, { useState } from 'react';
2+
import { View, Text, ScrollView, TouchableOpacity, TextInput, StyleSheet, Alert } from 'react-native';
3+
import { SafeAreaView } from 'react-native-safe-area-context';
4+
import { Check, ChevronRight } from 'lucide-react-native';
5+
import { colors } from '../theme/colors';
6+
import { useApp } from '../context/AppContext';
7+
8+
const STEPS = ['MY BAG', 'ADDRESS', 'PAYMENT'];
9+
const PAYMENT_OPTIONS = [
10+
{ id: 'cod', label: 'Cash on Delivery' },
11+
{ id: 'upi', label: 'UPI / GPay / PhonePe' },
12+
{ id: 'card', label: 'Credit / Debit Card' },
13+
];
14+
15+
export default function CheckoutScreen({ navigation }) {
16+
const { cartItems, cartTotal, placeOrder } = useApp();
17+
const [step, setStep] = useState(0);
18+
const [address, setAddress] = useState({ name: '', phone: '', street: '', pincode: '', city: '', state: '' });
19+
const [payment, setPayment] = useState('cod');
20+
const [done, setDone] = useState(false);
21+
const shipping = cartTotal > 999 ? 0 : 99;
22+
23+
const handlePlaceOrder = async () => {
24+
await placeOrder(cartItems);
25+
setDone(true);
26+
};
27+
28+
if (done) {
29+
return (
30+
<SafeAreaView style={styles.safe}>
31+
<View style={styles.successBox}>
32+
<View style={styles.successIcon}><Check size={36} color={colors.white} /></View>
33+
<Text style={styles.successTitle}>Order Placed!</Text>
34+
<Text style={styles.successMsg}>Your order has been placed successfully. You'll receive a confirmation soon.</Text>
35+
<TouchableOpacity style={styles.continueBtn} onPress={() => navigation.navigate('Home')}>
36+
<Text style={styles.continueTxt}>CONTINUE SHOPPING</Text>
37+
</TouchableOpacity>
38+
</View>
39+
</SafeAreaView>
40+
);
41+
}
42+
43+
return (
44+
<SafeAreaView style={styles.safe}>
45+
<View style={styles.topBar}>
46+
<Text style={styles.title}>CHECKOUT</Text>
47+
</View>
48+
49+
<View style={styles.stepsRow}>
50+
{STEPS.map((s, i) => (
51+
<React.Fragment key={s}>
52+
<View style={styles.stepItem}>
53+
<View style={[styles.stepCircle, i <= step && styles.stepCircleActive, i < step && styles.stepCircleDone]}>
54+
{i < step
55+
? <Check size={12} color={colors.white} />
56+
: <Text style={[styles.stepNum, i <= step && styles.stepNumActive]}>{i + 1}</Text>
57+
}
58+
</View>
59+
<Text style={[styles.stepLabel, i <= step && styles.stepLabelActive]}>{s}</Text>
60+
</View>
61+
{i < STEPS.length - 1 && <View style={[styles.stepLine, i < step && styles.stepLineDone]} />}
62+
</React.Fragment>
63+
))}
64+
</View>
65+
66+
<ScrollView contentContainerStyle={styles.body} showsVerticalScrollIndicator={false}>
67+
68+
{step === 0 && (
69+
<View>
70+
{cartItems.map((item, i) => (
71+
<View key={i} style={styles.bagItem}>
72+
<View style={styles.bagInfo}>
73+
<Text style={styles.bagName}>{item.name}</Text>
74+
<Text style={styles.bagMeta}>Size: UK {item.size} · Qty: {item.qty}</Text>
75+
</View>
76+
<Text style={styles.bagPrice}>{(item.price * item.qty).toLocaleString('en-IN')}</Text>
77+
</View>
78+
))}
79+
<View style={styles.billing}>
80+
<View style={styles.billRow}><Text style={styles.billLabel}>Cart Total</Text><Text style={styles.billVal}>{cartTotal.toLocaleString('en-IN')}</Text></View>
81+
<View style={styles.billRow}><Text style={styles.billLabel}>Shipping</Text><Text style={[styles.billVal, { color: colors.success }]}>{shipping === 0 ? 'FREE' : `₹ ${shipping}`}</Text></View>
82+
<View style={[styles.billRow, styles.totalRow]}><Text style={styles.totalLabel}>Total</Text><Text style={styles.totalVal}>{(cartTotal + shipping).toLocaleString('en-IN')}</Text></View>
83+
</View>
84+
<TouchableOpacity style={styles.nextBtn} onPress={() => setStep(1)}>
85+
<Text style={styles.nextTxt}>PROCEED TO ADDRESS</Text>
86+
<ChevronRight size={18} color={colors.white} />
87+
</TouchableOpacity>
88+
</View>
89+
)}
90+
91+
{step === 1 && (
92+
<View>
93+
<Text style={styles.sectionTitle}>DELIVERY ADDRESS</Text>
94+
<View style={styles.formRow}>
95+
<TextInput style={[styles.input, { flex: 1 }]} placeholder="Full Name *" value={address.name} onChangeText={v => setAddress({ ...address, name: v })} />
96+
<TextInput style={[styles.input, { flex: 1 }]} placeholder="Phone *" value={address.phone} onChangeText={v => setAddress({ ...address, phone: v })} keyboardType="phone-pad" />
97+
</View>
98+
<TextInput style={styles.input} placeholder="Street Address *" value={address.street} onChangeText={v => setAddress({ ...address, street: v })} />
99+
<View style={styles.formRow}>
100+
<TextInput style={[styles.input, { flex: 1 }]} placeholder="Pincode *" value={address.pincode} onChangeText={v => setAddress({ ...address, pincode: v })} keyboardType="numeric" />
101+
<TextInput style={[styles.input, { flex: 1 }]} placeholder="City *" value={address.city} onChangeText={v => setAddress({ ...address, city: v })} />
102+
<TextInput style={[styles.input, { flex: 1 }]} placeholder="State *" value={address.state} onChangeText={v => setAddress({ ...address, state: v })} />
103+
</View>
104+
<View style={styles.btnRow}>
105+
<TouchableOpacity style={styles.backBtn} onPress={() => setStep(0)}><Text style={styles.backTxt}>BACK</Text></TouchableOpacity>
106+
<TouchableOpacity style={styles.nextBtn} onPress={() => {
107+
if (!address.name || !address.phone || !address.street || !address.city || !address.pincode) {
108+
Alert.alert('Missing Info', 'Please fill all required fields'); return;
109+
}
110+
setStep(2);
111+
}}>
112+
<Text style={styles.nextTxt}>PROCEED TO PAYMENT</Text>
113+
<ChevronRight size={18} color={colors.white} />
114+
</TouchableOpacity>
115+
</View>
116+
</View>
117+
)}
118+
119+
{step === 2 && (
120+
<View>
121+
<Text style={styles.sectionTitle}>PAYMENT METHOD</Text>
122+
{PAYMENT_OPTIONS.map(opt => (
123+
<TouchableOpacity
124+
key={opt.id}
125+
style={[styles.payOption, payment === opt.id && styles.payOptionActive]}
126+
onPress={() => setPayment(opt.id)}
127+
>
128+
<View style={[styles.radio, payment === opt.id && styles.radioActive]}>
129+
{payment === opt.id && <View style={styles.radioDot} />}
130+
</View>
131+
<Text style={styles.payLabel}>{opt.label}</Text>
132+
</TouchableOpacity>
133+
))}
134+
<View style={styles.billing}>
135+
<View style={styles.billRow}><Text style={styles.billLabel}>Total</Text><Text style={styles.totalVal}>{(cartTotal + shipping).toLocaleString('en-IN')}</Text></View>
136+
</View>
137+
<View style={styles.btnRow}>
138+
<TouchableOpacity style={styles.backBtn} onPress={() => setStep(1)}><Text style={styles.backTxt}>BACK</Text></TouchableOpacity>
139+
<TouchableOpacity style={[styles.nextBtn, { flex: 2 }]} onPress={handlePlaceOrder}>
140+
<Text style={styles.nextTxt}>PLACE ORDER</Text>
141+
</TouchableOpacity>
142+
</View>
143+
</View>
144+
)}
145+
146+
</ScrollView>
147+
</SafeAreaView>
148+
);
149+
}
150+
151+
const styles = StyleSheet.create({
152+
safe: { flex: 1, backgroundColor: colors.white },
153+
topBar: { paddingHorizontal: 20, paddingVertical: 14, borderBottomWidth: 1, borderBottomColor: colors.border },
154+
title: { fontSize: 16, fontWeight: '800', letterSpacing: 1.5, color: colors.dark },
155+
stepsRow: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, paddingVertical: 16 },
156+
stepItem: { alignItems: 'center', gap: 4 },
157+
stepCircle: { width: 28, height: 28, borderRadius: 14, borderWidth: 2, borderColor: colors.border, alignItems: 'center', justifyContent: 'center' },
158+
stepCircleActive: { borderColor: colors.brand },
159+
stepCircleDone: { backgroundColor: colors.brand, borderColor: colors.brand },
160+
stepNum: { fontSize: 12, fontWeight: '700', color: colors.gray },
161+
stepNumActive: { color: colors.brand },
162+
stepLabel: { fontSize: 10, fontWeight: '700', color: colors.gray, letterSpacing: 0.5 },
163+
stepLabelActive: { color: colors.brand },
164+
stepLine: { flex: 1, height: 2, backgroundColor: colors.border, marginBottom: 16 },
165+
stepLineDone: { backgroundColor: colors.brand },
166+
body: { padding: 20, paddingBottom: 40 },
167+
bagItem: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: colors.border },
168+
bagInfo: { flex: 1 },
169+
bagName: { fontSize: 13, fontWeight: '700', color: colors.dark, marginBottom: 4 },
170+
bagMeta: { fontSize: 12, color: colors.gray },
171+
bagPrice: { fontSize: 14, fontWeight: '800', color: colors.dark },
172+
billing: { backgroundColor: colors.lightGray, borderRadius: 12, padding: 16, marginVertical: 16, gap: 8 },
173+
billRow: { flexDirection: 'row', justifyContent: 'space-between' },
174+
billLabel: { fontSize: 14, color: colors.gray },
175+
billVal: { fontSize: 14, fontWeight: '600', color: colors.dark },
176+
totalRow: { paddingTop: 8, borderTopWidth: 1, borderTopColor: colors.border, marginTop: 4 },
177+
totalLabel: { fontSize: 15, fontWeight: '800', color: colors.dark },
178+
totalVal: { fontSize: 16, fontWeight: '900', color: colors.dark },
179+
nextBtn: { flex: 1, backgroundColor: colors.brand, borderRadius: 12, paddingVertical: 14, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 6 },
180+
nextTxt: { color: colors.white, fontWeight: '800', fontSize: 13, letterSpacing: 0.5 },
181+
btnRow: { flexDirection: 'row', gap: 10, marginTop: 8 },
182+
backBtn: { paddingHorizontal: 20, paddingVertical: 14, borderRadius: 12, borderWidth: 1.5, borderColor: colors.border, alignItems: 'center', justifyContent: 'center' },
183+
backTxt: { fontSize: 13, fontWeight: '700', color: colors.gray },
184+
sectionTitle: { fontSize: 13, fontWeight: '800', letterSpacing: 1, color: colors.dark, marginBottom: 16 },
185+
formRow: { flexDirection: 'row', gap: 10 },
186+
input: { borderWidth: 1.5, borderColor: colors.border, borderRadius: 10, paddingHorizontal: 14, paddingVertical: 12, fontSize: 14, color: colors.dark, marginBottom: 12 },
187+
payOption: { flexDirection: 'row', alignItems: 'center', gap: 12, padding: 14, borderWidth: 1.5, borderColor: colors.border, borderRadius: 12, marginBottom: 10 },
188+
payOptionActive: { borderColor: colors.brand, backgroundColor: colors.brandLight },
189+
radio: { width: 18, height: 18, borderRadius: 9, borderWidth: 2, borderColor: colors.border, alignItems: 'center', justifyContent: 'center' },
190+
radioActive: { borderColor: colors.brand },
191+
radioDot: { width: 8, height: 8, borderRadius: 4, backgroundColor: colors.brand },
192+
payLabel: { fontSize: 14, fontWeight: '500', color: colors.dark },
193+
successBox: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 32, gap: 16 },
194+
successIcon: { width: 72, height: 72, borderRadius: 36, backgroundColor: colors.brand, alignItems: 'center', justifyContent: 'center' },
195+
successTitle: { fontSize: 24, fontWeight: '800', color: colors.dark },
196+
successMsg: { fontSize: 14, color: colors.gray, textAlign: 'center', lineHeight: 22 },
197+
continueBtn: { backgroundColor: colors.brand, paddingHorizontal: 32, paddingVertical: 14, borderRadius: 12, marginTop: 8 },
198+
continueTxt: { color: colors.white, fontWeight: '800', fontSize: 14, letterSpacing: 1 },
199+
});

0 commit comments

Comments
 (0)