This repository has been archived on 2026-08-11. You can view files and clone it, but cannot push or open issues or pull requests.
Gishen-Mob/app/(root)/(screens)/profile.tsx
kirukib 5391780b9b Ship Rx Care tab, notifications inbox, and guest pickup codes.
Replace the profile tab with prescription management (inline reminders and family invite), add a shared tab top bar, separate notifications from orders, and support any-branch guest checkout with GSH codes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 15:35:51 +03:00

305 lines
12 KiB
TypeScript

import { Ionicons } from "@expo/vector-icons";
import * as ImagePicker from "expo-image-picker";
import { router } from "expo-router";
import { useState } from "react";
import { Alert, Linking, Pressable, ScrollView, View } from "react-native";
import { useTranslation } from "react-i18next";
import { Avatar } from "@/components/ui/Avatar";
import { Badge } from "@/components/ui/Badge";
import { Button } from "@/components/ui/Button";
import { Card, MenuRow } from "@/components/ui/Card";
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 { routes } from "@/lib/routes";
import { useAuthStore } from "@/lib/stores/authStore";
import { useLangStore } from "@/lib/stores/langStore";
import { useProfileModeStore } from "@/lib/stores/profileModeStore";
import { colors, gutter } from "@/lib/theme";
import { CALL_PHONE, CHATWOOT_BASE, WHATSAPP_NUMBER } from "@/lib/utils";
export default function ProfileScreen() {
const { t } = useTranslation();
const user = useAuthStore((s) => s.user);
const setAvatar = useAuthStore((s) => s.setAvatar);
const updateProfile = useAuthStore((s) => s.updateProfile);
const signOut = useAuthStore((s) => s.signOut);
const setHasOnboarded = useAuthStore((s) => s.setHasOnboarded);
const mode = useProfileModeStore((s) => s.mode);
const membership = useProfileModeStore((s) => s.activeMembership());
const language = useLangStore((s) => s.language);
const setLanguage = useLangStore((s) => s.setLanguage);
const [editing, setEditing] = useState(false);
const [name, setName] = useState(user?.name ?? "");
const [phone, setPhone] = useState(user?.phone ?? "");
const [email, setEmail] = useState(user?.email ?? "");
const pickAvatar = async () => {
const permission = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (!permission.granted) {
Alert.alert(
"Photo access needed",
"Allow photo library access to set your profile picture."
);
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ["images"],
allowsEditing: true,
aspect: [1, 1],
quality: 0.85,
});
if (!result.canceled && result.assets[0]?.uri) {
setAvatar(result.assets[0].uri);
}
};
const takePhoto = async () => {
const permission = await ImagePicker.requestCameraPermissionsAsync();
if (!permission.granted) {
Alert.alert("Camera access needed", "Allow camera access to take a profile photo.");
return;
}
const result = await ImagePicker.launchCameraAsync({
allowsEditing: true,
aspect: [1, 1],
quality: 0.85,
});
if (!result.canceled && result.assets[0]?.uri) {
setAvatar(result.assets[0].uri);
}
};
const changePhoto = () => {
Alert.alert("Profile photo", "Choose how you'd like to update your photo.", [
{ text: "Choose from library", onPress: pickAvatar },
{ text: "Take photo", onPress: takePhoto },
...(user?.avatarUri
? [{ text: "Remove photo", style: "destructive" as const, onPress: () => setAvatar(null) }]
: []),
{ text: "Cancel", style: "cancel" },
]);
};
const links: {
label: string;
href: string;
icon: keyof typeof Ionicons.glyphMap;
subtitle?: string;
}[] = [
{ label: t("common.switchProfile"), href: routes.profileSwitch, icon: "swap-horizontal", subtitle: mode },
{ label: t("common.joinOrg"), href: routes.profileJoin, icon: "business-outline" },
{ label: t("common.orders"), href: routes.orders, icon: "receipt-outline", subtitle: "Track delivery" },
{ label: t("common.notifications"), href: routes.notifications, icon: "notifications-outline", subtitle: "Alerts inbox" },
{ label: t("common.prescriptions"), href: routes.rxList, icon: "medkit-outline", subtitle: "Rx + family" },
{ label: t("common.loyalty"), href: routes.loyalty, icon: "star-outline", subtitle: `${user?.points ?? 0} pts` },
{ label: t("common.subscriptions"), href: routes.subscriptions, icon: "repeat-outline" },
{ label: t("common.emergencyContacts"), href: routes.emergencyContacts, icon: "alert-circle-outline" },
{ label: t("common.branches"), href: routes.branches, icon: "location-outline" },
{ label: t("common.addresses"), href: routes.addresses, icon: "home-outline" },
{ label: t("common.language"), href: routes.language, icon: "language-outline", subtitle: language.toUpperCase() },
{ label: t("common.about"), href: routes.about, icon: "information-circle-outline" },
{ label: t("common.contact"), href: routes.contact, icon: "mail-outline" },
];
return (
<Screen>
<AppHeader title={t("common.profile")} subtitle="Account and preferences" />
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={{
paddingBottom: 40,
paddingHorizontal: gutter,
}}
>
<Card style={{ marginTop: 4 }}>
<View style={{ flexDirection: "row", alignItems: "center", gap: 14 }}>
<Pressable onPress={changePhoto}>
<View>
<Avatar name={user?.name} uri={user?.avatarUri} size={72} />
<View
style={{
position: "absolute",
right: -2,
bottom: -2,
width: 26,
height: 26,
borderRadius: 13,
backgroundColor: colors.brand,
borderWidth: 2,
borderColor: colors.surface,
alignItems: "center",
justifyContent: "center",
}}
>
<Ionicons name="camera" size={13} color={colors.ink} />
</View>
</View>
</Pressable>
<View style={{ flex: 1 }}>
<Text variant="h2">{user?.name || "Guest"}</Text>
<Text variant="bodySm" color={colors.muted} style={{ marginTop: 2 }}>
{user?.phone || "—"}
</Text>
{user?.email ? (
<Text variant="caption" color={colors.subtle} style={{ marginTop: 2 }}>
{user.email}
</Text>
) : null}
<View style={{ flexDirection: "row", gap: 8, marginTop: 8, flexWrap: "wrap" }}>
<Badge
label={
mode === "corporate"
? membership?.orgName || t("common.corporate")
: t("common.personal")
}
tone="brand"
/>
<Badge label={`${user?.tier ?? "bronze"} · ${user?.points ?? 0} pts`} tone="neutral" />
</View>
</View>
</View>
<View style={{ flexDirection: "row", gap: 12, marginTop: 14 }}>
<Pressable onPress={changePhoto}>
<Text variant="label" color={colors.brandText}>
{user?.avatarUri ? "Change photo" : "Add photo"}
</Text>
</Pressable>
{!user?.isGuest ? (
<Pressable
onPress={() => {
setName(user?.name ?? "");
setPhone(user?.phone ?? "");
setEmail(user?.email ?? "");
setEditing((v) => !v);
}}
>
<Text variant="label" color={colors.brandText}>
{editing ? "Close editor" : "Edit profile"}
</Text>
</Pressable>
) : null}
</View>
{membership ? (
<Text variant="caption" color={colors.subtle} style={{ marginTop: 8 }}>
{membership.planName} · {membership.remainingAllowance} ETB allowance left
</Text>
) : null}
</Card>
{editing && !user?.isGuest ? (
<Card elevated={false} style={{ marginTop: 12 }}>
<Text variant="h3" style={{ marginBottom: 12 }}>
Edit profile
</Text>
<View style={{ gap: 12 }}>
<Field label="Full name" value={name} onChangeText={setName} />
<Field
label="Phone"
value={phone}
onChangeText={setPhone}
keyboardType="phone-pad"
/>
<Field
label="Email"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
/>
<Button
title="Save changes"
onPress={() => {
if (!name.trim()) {
Alert.alert("Name required");
return;
}
updateProfile({
name: name.trim(),
phone: phone.trim(),
email: email.trim() || undefined,
});
setEditing(false);
Alert.alert("Saved", "Your profile was updated.");
}}
/>
</View>
</Card>
) : null}
<View style={{ flexDirection: "row", gap: 8, marginTop: 14 }}>
{[
{ label: t("common.call"), icon: "call-outline" as const, href: `tel:${CALL_PHONE}` },
{
label: t("common.whatsapp"),
icon: "logo-whatsapp" as const,
href: `https://wa.me/${WHATSAPP_NUMBER}`,
},
{ label: t("common.chat"), icon: "chatbubble-ellipses-outline" as const, href: CHATWOOT_BASE },
].map((item) => (
<Pressable
key={item.label}
onPress={() => Linking.openURL(item.href)}
style={{
flex: 1,
backgroundColor: colors.surface,
borderRadius: 16,
borderWidth: 1,
borderColor: colors.line,
paddingVertical: 12,
alignItems: "center",
gap: 4,
}}
>
<Ionicons name={item.icon} size={18} color={colors.brandText} />
<Text variant="micro">{item.label}</Text>
</Pressable>
))}
</View>
<Card padded={false} style={{ marginTop: 18, overflow: "hidden" }}>
<MenuRow
icon="language-outline"
title={t("common.language")}
subtitle={language === "en" ? "English" : "አማርኛ"}
onPress={() => setLanguage(language === "en" ? "am" : "en")}
right={
<Text variant="label" color={colors.brandText}>
{language.toUpperCase()}
</Text>
}
/>
{links.map((l, index) => (
<MenuRow
key={l.href}
icon={l.icon}
title={l.label}
subtitle={l.subtitle}
onPress={() => router.push(l.href as never)}
last={index === links.length - 1}
/>
))}
</Card>
<Card padded={false} style={{ marginTop: 12, overflow: "hidden" }}>
<MenuRow
icon="log-out-outline"
title="Sign out"
tone="danger"
last
onPress={() => {
signOut();
setHasOnboarded(false);
router.replace(routes.welcome);
}}
/>
</Card>
</ScrollView>
</Screen>
);
}