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>
256 lines
9.4 KiB
TypeScript
256 lines
9.4 KiB
TypeScript
import { useMemo, useState } from "react";
|
|
import { Alert, ScrollView, View } from "react-native";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Badge } from "@/components/ui/Badge";
|
|
import { Button } from "@/components/ui/Button";
|
|
import { Card } from "@/components/ui/Card";
|
|
import { Chip, Segmented } from "@/components/ui/Chip";
|
|
import { Field } from "@/components/ui/Field";
|
|
import { AppHeader } from "@/components/ui/Header";
|
|
import { Screen } from "@/components/ui/Screen";
|
|
import { Text } from "@/components/ui/Text";
|
|
import { products } from "@/lib/data/products";
|
|
import { useFamilyStore } from "@/lib/stores/familyStore";
|
|
import { useLocationStore, type Fulfillment, type PaymentMethod } from "@/lib/stores/locationStore";
|
|
import { useSubscriptionStore } from "@/lib/stores/subscriptionStore";
|
|
import { colors, gutter } from "@/lib/theme";
|
|
|
|
const FREQUENCIES = [
|
|
{ days: 7, label: "Weekly" },
|
|
{ days: 14, label: "Every 2 weeks" },
|
|
{ days: 30, label: "Monthly" },
|
|
{ days: 60, label: "Every 2 months" },
|
|
{ days: 90, label: "Every 3 months" },
|
|
];
|
|
|
|
export default function SubscriptionsScreen() {
|
|
const { t } = useTranslation();
|
|
const items = useSubscriptionStore((s) => s.items);
|
|
const add = useSubscriptionStore((s) => s.add);
|
|
const pause = useSubscriptionStore((s) => s.pause);
|
|
const resume = useSubscriptionStore((s) => s.resume);
|
|
const cancel = useSubscriptionStore((s) => s.cancel);
|
|
const paymentPref = useLocationStore((s) => s.paymentPreference);
|
|
const fulfillmentPref = useLocationStore((s) => s.fulfillment);
|
|
const members = useFamilyStore((s) => s.members);
|
|
|
|
const refillCandidates = useMemo(
|
|
() =>
|
|
products.filter(
|
|
(p) =>
|
|
p.requiresPrescription ||
|
|
p.category === "diabetic" ||
|
|
p.category === "vitamins" ||
|
|
p.badge === "bestseller"
|
|
),
|
|
[]
|
|
);
|
|
|
|
const [showCreate, setShowCreate] = useState(false);
|
|
const [productId, setProductId] = useState(refillCandidates[0]?.id ?? products[0].id);
|
|
const [qty, setQty] = useState("1");
|
|
const [frequencyDays, setFrequencyDays] = useState(30);
|
|
const [fulfillment, setFulfillment] = useState<Fulfillment>(fulfillmentPref);
|
|
const [payment, setPayment] = useState<PaymentMethod>(
|
|
paymentPref === "pay_at_branch" && fulfillmentPref === "delivery"
|
|
? "chapa"
|
|
: paymentPref
|
|
);
|
|
const [memberId, setMemberId] = useState<string | null>(null);
|
|
const [query, setQuery] = useState("");
|
|
|
|
const filtered = refillCandidates.filter(
|
|
(p) =>
|
|
!query ||
|
|
p.name.toLowerCase().includes(query.toLowerCase()) ||
|
|
p.brand.toLowerCase().includes(query.toLowerCase())
|
|
);
|
|
const selected = products.find((p) => p.id === productId) ?? products[0];
|
|
|
|
return (
|
|
<Screen>
|
|
<AppHeader
|
|
title={t("common.subscriptions")}
|
|
subtitle="Auto-refill chronic medicines on a schedule"
|
|
/>
|
|
<ScrollView contentContainerStyle={{ paddingHorizontal: gutter, paddingBottom: 40, gap: 12 }}>
|
|
{!showCreate ? (
|
|
<Button
|
|
title="Create refill subscription"
|
|
icon="add-circle-outline"
|
|
onPress={() => setShowCreate(true)}
|
|
/>
|
|
) : (
|
|
<Card elevated={false}>
|
|
<Text variant="h3" style={{ marginBottom: 12 }}>
|
|
New subscription
|
|
</Text>
|
|
<Field
|
|
label="Search product"
|
|
value={query}
|
|
onChangeText={setQuery}
|
|
placeholder="Metformin, Vitamin C…"
|
|
icon="search-outline"
|
|
/>
|
|
<ScrollView
|
|
horizontal
|
|
showsHorizontalScrollIndicator={false}
|
|
style={{ marginTop: 12 }}
|
|
contentContainerStyle={{ gap: 8 }}
|
|
>
|
|
{filtered.slice(0, 12).map((p) => (
|
|
<Chip
|
|
key={p.id}
|
|
label={p.name}
|
|
selected={productId === p.id}
|
|
onPress={() => setProductId(p.id)}
|
|
/>
|
|
))}
|
|
</ScrollView>
|
|
<Text variant="caption" color={colors.subtle} style={{ marginTop: 8 }}>
|
|
Selected: {selected.name} · {selected.unit}
|
|
</Text>
|
|
|
|
<View style={{ marginTop: 14, gap: 12 }}>
|
|
<Field
|
|
label="Quantity"
|
|
value={qty}
|
|
onChangeText={setQty}
|
|
keyboardType="number-pad"
|
|
/>
|
|
<Text variant="label" color={colors.muted}>
|
|
Frequency
|
|
</Text>
|
|
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 8 }}>
|
|
{FREQUENCIES.map((f) => (
|
|
<Chip
|
|
key={f.days}
|
|
label={f.label}
|
|
selected={frequencyDays === f.days}
|
|
onPress={() => setFrequencyDays(f.days)}
|
|
/>
|
|
))}
|
|
</View>
|
|
|
|
<Text variant="label" color={colors.muted}>
|
|
Fulfillment
|
|
</Text>
|
|
<Segmented
|
|
value={fulfillment}
|
|
onChange={(v) => {
|
|
setFulfillment(v);
|
|
if (v === "delivery" && payment === "pay_at_branch") setPayment("chapa");
|
|
}}
|
|
options={[
|
|
{ value: "delivery", label: "Delivery" },
|
|
{ value: "pickup", label: "Pickup" },
|
|
]}
|
|
/>
|
|
|
|
<Text variant="label" color={colors.muted} style={{ marginTop: 4 }}>
|
|
Payment
|
|
</Text>
|
|
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 8 }}>
|
|
{(
|
|
[
|
|
"chapa",
|
|
"telebirr",
|
|
"mpesa",
|
|
...(fulfillment === "pickup" ? (["pay_at_branch"] as const) : []),
|
|
] as PaymentMethod[]
|
|
).map((m) => (
|
|
<Chip
|
|
key={m}
|
|
label={m}
|
|
selected={payment === m}
|
|
onPress={() => setPayment(m)}
|
|
/>
|
|
))}
|
|
</View>
|
|
|
|
<Text variant="label" color={colors.muted}>
|
|
For family member (optional)
|
|
</Text>
|
|
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 8 }}>
|
|
<Chip label="Me" selected={!memberId} onPress={() => setMemberId(null)} />
|
|
{members
|
|
.filter((m) => m.status === "accepted")
|
|
.map((m) => (
|
|
<Chip
|
|
key={m.id}
|
|
label={m.name}
|
|
selected={memberId === m.id}
|
|
onPress={() => setMemberId(m.id)}
|
|
/>
|
|
))}
|
|
</View>
|
|
|
|
<Button
|
|
title="Start subscription"
|
|
onPress={() => {
|
|
add({
|
|
productId: selected.id,
|
|
productName: selected.name,
|
|
quantity: Math.max(1, Number(qty) || 1),
|
|
frequencyDays,
|
|
fulfillment,
|
|
paymentMethod: payment,
|
|
memberId,
|
|
});
|
|
setShowCreate(false);
|
|
Alert.alert(
|
|
"Subscription active",
|
|
`${selected.name} every ${frequencyDays} days via ${fulfillment}.`
|
|
);
|
|
}}
|
|
/>
|
|
<Button title="Cancel" variant="ghost" onPress={() => setShowCreate(false)} />
|
|
</View>
|
|
</Card>
|
|
)}
|
|
|
|
{items.map((s) => (
|
|
<Card key={s.id}>
|
|
<View style={{ flexDirection: "row", justifyContent: "space-between", gap: 12 }}>
|
|
<View style={{ flex: 1 }}>
|
|
<Text variant="h3">{s.productName}</Text>
|
|
<Text variant="caption" color={colors.subtle} style={{ marginTop: 4 }}>
|
|
Every {s.frequencyDays} days · Qty {s.quantity} · {s.fulfillment}
|
|
</Text>
|
|
<Text variant="bodySm" color={colors.muted} style={{ marginTop: 6 }}>
|
|
Next: {new Date(s.nextFulfillmentAt).toLocaleDateString()} · {s.paymentMethod}
|
|
{s.memberId
|
|
? ` · ${members.find((m) => m.id === s.memberId)?.name ?? "Family"}`
|
|
: ""}
|
|
</Text>
|
|
</View>
|
|
<Badge
|
|
label={s.status}
|
|
tone={
|
|
s.status === "active" ? "brand" : s.status === "paused" ? "warn" : "danger"
|
|
}
|
|
/>
|
|
</View>
|
|
<View style={{ flexDirection: "row", gap: 10, marginTop: 14 }}>
|
|
{s.status === "active" ? (
|
|
<Button title="Pause" size="sm" variant="outline" fullWidth={false} onPress={() => pause(s.id)} />
|
|
) : s.status === "paused" ? (
|
|
<Button title="Resume" size="sm" fullWidth={false} onPress={() => resume(s.id)} />
|
|
) : null}
|
|
{s.status !== "cancelled" ? (
|
|
<Button
|
|
title="Cancel"
|
|
size="sm"
|
|
variant="ghost"
|
|
fullWidth={false}
|
|
onPress={() => cancel(s.id)}
|
|
/>
|
|
) : null}
|
|
</View>
|
|
</Card>
|
|
))}
|
|
</ScrollView>
|
|
</Screen>
|
|
);
|
|
}
|