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)/(tabs)/profile.tsx
kirukib 7310a349c5 Add Gishen mobile app with complete demo shopping and care flows.
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>
2026-08-07 01:21:32 +03:00

308 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 { useSafeAreaInsets } from "react-native-safe-area-context";
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 { Screen, TAB_BAR_INSET } 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 insets = useSafeAreaInsets();
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.prescriptions"), href: routes.rxList, icon: "document-text-outline" },
{ label: t("common.loyalty"), href: routes.loyalty, icon: "star-outline", subtitle: `${user?.points ?? 0} pts` },
{ label: t("common.family"), href: routes.family, icon: "people-outline" },
{ 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>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={{
paddingTop: insets.top + 16,
paddingBottom: TAB_BAR_INSET + 24,
paddingHorizontal: gutter,
}}
>
<Text variant="h1">{t("common.profile")}</Text>
<Card style={{ marginTop: 16 }}>
<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>
);
}