Ship Expo Router screens, seeded stores, and polished UX so auth, shop, checkout, prescriptions, loyalty, family, and account processes work end-to-end offline. Co-authored-by: Cursor <cursoragent@cursor.com>
184 lines
6.7 KiB
TypeScript
184 lines
6.7 KiB
TypeScript
import { Ionicons } from "@expo/vector-icons";
|
|
import { router } from "expo-router";
|
|
import {
|
|
Alert,
|
|
Image,
|
|
Pressable,
|
|
ScrollView,
|
|
TextInput,
|
|
View,
|
|
} from "react-native";
|
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Button } from "@/components/ui/Button";
|
|
import { Card, InfoRow } from "@/components/ui/Card";
|
|
import { EmptyState } from "@/components/ui/EmptyState";
|
|
import { QtyStepper } from "@/components/ui/QtyStepper";
|
|
import { Screen, TAB_BAR_INSET } from "@/components/ui/Screen";
|
|
import { StickyBar } from "@/components/ui/StickyBar";
|
|
import { Text } from "@/components/ui/Text";
|
|
import { DELIVERY_FEE, DEMO_VOUCHER, voucherDiscount } from "@/lib/commerce";
|
|
import { routes } from "@/lib/routes";
|
|
import { useCartStore } from "@/lib/stores/cartStore";
|
|
import { colors, fonts, gutter, radii } from "@/lib/theme";
|
|
import { formatETB } from "@/lib/utils";
|
|
|
|
export default function CartScreen() {
|
|
const { t } = useTranslation();
|
|
const insets = useSafeAreaInsets();
|
|
const items = useCartStore((s) => s.items);
|
|
const setQuantity = useCartStore((s) => s.setQuantity);
|
|
const removeItem = useCartStore((s) => s.removeItem);
|
|
const subtotal = useCartStore((s) => s.subtotal());
|
|
const voucher = useCartStore((s) => s.voucherCode);
|
|
const setVoucher = useCartStore((s) => s.setVoucher);
|
|
const delivery = items.length ? DELIVERY_FEE : 0;
|
|
const discount = voucherDiscount(subtotal, voucher);
|
|
const total = subtotal + delivery - discount;
|
|
|
|
return (
|
|
<Screen>
|
|
<ScrollView
|
|
contentContainerStyle={{
|
|
paddingTop: insets.top + 16,
|
|
paddingBottom: TAB_BAR_INSET + 100,
|
|
paddingHorizontal: gutter,
|
|
}}
|
|
>
|
|
<Text variant="h1">{t("common.cart")}</Text>
|
|
<Text variant="bodySm" color={colors.subtle} style={{ marginTop: 4 }}>
|
|
{items.length} item{items.length === 1 ? "" : "s"} in your bag
|
|
</Text>
|
|
|
|
{items.length === 0 ? (
|
|
<EmptyState
|
|
icon="bag-handle-outline"
|
|
title={t("common.emptyCart")}
|
|
body="Browse the shop and add medicines or wellness products."
|
|
actionTitle="Explore products"
|
|
onAction={() => router.push(routes.shop)}
|
|
/>
|
|
) : (
|
|
<View style={{ marginTop: 16, gap: 12 }}>
|
|
{items.map((item) => (
|
|
<Card key={item.productId} elevated>
|
|
<View style={{ flexDirection: "row", gap: 12 }}>
|
|
<Image
|
|
source={{ uri: item.image }}
|
|
style={{
|
|
width: 72,
|
|
height: 72,
|
|
borderRadius: radii.sm,
|
|
backgroundColor: colors.surfaceAlt,
|
|
}}
|
|
resizeMode="contain"
|
|
/>
|
|
<View style={{ flex: 1 }}>
|
|
<View style={{ flexDirection: "row", justifyContent: "space-between" }}>
|
|
<Text variant="h3" style={{ flex: 1 }} numberOfLines={2}>
|
|
{item.name}
|
|
</Text>
|
|
<Pressable onPress={() => removeItem(item.productId)} hitSlop={8}>
|
|
<Ionicons name="trash-outline" size={18} color={colors.subtle} />
|
|
</Pressable>
|
|
</View>
|
|
<Text variant="caption" color={colors.subtle} style={{ marginTop: 2 }}>
|
|
{item.unit}
|
|
{item.requiresPrescription ? " · RX" : ""}
|
|
</Text>
|
|
<View
|
|
style={{
|
|
marginTop: 10,
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
}}
|
|
>
|
|
<Text variant="price">{formatETB(item.price)}</Text>
|
|
<QtyStepper
|
|
size="sm"
|
|
value={item.quantity}
|
|
onChange={(n) => setQuantity(item.productId, n)}
|
|
min={0}
|
|
/>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</Card>
|
|
))}
|
|
|
|
<Pressable onPress={() => router.push(routes.shop)}>
|
|
<Text variant="label" color={colors.brandText}>
|
|
+ Add product
|
|
</Text>
|
|
</Pressable>
|
|
|
|
<Card elevated={false} style={{ marginTop: 8 }}>
|
|
<Text variant="h3" style={{ marginBottom: 8 }}>
|
|
Voucher
|
|
</Text>
|
|
<View
|
|
style={{
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
borderWidth: 1,
|
|
borderColor: colors.line,
|
|
borderRadius: radii.pill,
|
|
paddingLeft: 14,
|
|
paddingRight: 6,
|
|
height: 48,
|
|
}}
|
|
>
|
|
<TextInput
|
|
value={voucher ?? ""}
|
|
onChangeText={(v) => setVoucher(v || null)}
|
|
placeholder="Try GISHEN10"
|
|
placeholderTextColor={colors.subtle}
|
|
style={{
|
|
flex: 1,
|
|
fontFamily: fonts.regular,
|
|
fontSize: 14,
|
|
color: colors.ink,
|
|
}}
|
|
/>
|
|
<Button
|
|
title="Apply"
|
|
size="sm"
|
|
fullWidth={false}
|
|
onPress={() => {
|
|
if ((voucher ?? "").toUpperCase() === DEMO_VOUCHER) {
|
|
setVoucher(DEMO_VOUCHER);
|
|
Alert.alert("Applied", "10% off your basket.");
|
|
} else {
|
|
Alert.alert("Invalid code", `Use ${DEMO_VOUCHER} for a demo discount.`);
|
|
}
|
|
}}
|
|
/>
|
|
</View>
|
|
</Card>
|
|
|
|
<Card elevated={false}>
|
|
<InfoRow label="Payment" value={formatETB(subtotal)} />
|
|
<InfoRow label="Delivery" value={formatETB(delivery)} />
|
|
{discount > 0 ? (
|
|
<InfoRow label="Discount" value={`- ${formatETB(discount)}`} valueColor={colors.brandText} />
|
|
) : null}
|
|
<InfoRow label={t("common.total")} value={formatETB(total)} emphasis last />
|
|
</Card>
|
|
</View>
|
|
)}
|
|
</ScrollView>
|
|
|
|
{items.length > 0 ? (
|
|
<StickyBar>
|
|
<Button
|
|
title={t("common.payNow")}
|
|
iconRight="arrow-forward"
|
|
onPress={() => router.push(routes.checkout)}
|
|
/>
|
|
</StickyBar>
|
|
) : null}
|
|
</Screen>
|
|
);
|
|
}
|