import React, { useState, useEffect, useRef } from "react"; import { ScrollView, View, Keyboard, TouchableOpacity, Image, Alert, ActivityIndicator, } from "react-native"; import { Button } from "~/components/ui/button"; import { Input } from "~/components/ui/input"; import { Label } from "~/components/ui/label"; import { Text } from "~/components/ui/text"; import { router } from "expo-router"; import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile"; import { ROUTES } from "~/lib/routes"; import { profileUpdateSchema, validate } from "~/lib/utils/validationSchemas"; import BackButton from "~/components/ui/backButton"; import ScreenWrapper from "~/components/ui/ScreenWrapper"; import ModalToast from "~/components/ui/toast"; import { useTranslation } from "react-i18next"; import Dropdown, { DropdownOption } from "~/components/ui/dropdown"; import { useLangStore } from "~/lib/stores"; import { Copy, Plus, ChevronDown } from "lucide-react-native"; import * as Clipboard from "expo-clipboard"; import * as ImagePicker from "expo-image-picker"; import { Icons } from "~/assets/icons"; import { AuthService } from "~/lib/services/authServices"; import { uploadProfileImage } from "~/lib/services/profileImageService"; import BottomSheet from "~/components/ui/bottomSheet"; type ProfileLinkedAccount = { id: string; bankId: string; bankName: string; accountNumber: string; isDefault: boolean; }; const PROFILE_BANK_OPTIONS: { id: string; name: string }[] = [ { id: "cbe", name: "Commercial Bank of Ethiopia" }, { id: "dashen", name: "Dashen Bank" }, { id: "abay", name: "Abay Bank" }, { id: "awash", name: "Awash Bank" }, { id: "hibret", name: "Hibret Bank" }, { id: "telebirr", name: "Ethio Telecom (Telebirr)" }, { id: "safaricom", name: "Safaricom M-PESA" }, ]; export default function EditProfile() { const { t } = useTranslation(); const language = useLangStore((state) => state.language); const setLanguage = useLangStore((state) => state.setLanguage); const { user, profile, wallet, profileLoading, profileError } = useAuthWithProfile(); const [updateLoading, setUpdateLoading] = useState(false); const [updateError, setUpdateError] = useState(null); const [profileImage, setProfileImage] = useState(null); const [imagePicking, setImagePicking] = useState(false); // Editable state for form fields const [editedProfile, setEditedProfile] = useState({ fullName: "", phoneNumber: "", email: "", address: "", }); const [profileAccounts, setProfileAccounts] = useState< ProfileLinkedAccount[] >([]); const [isSelectAccountSheetVisible, setIsSelectAccountSheetVisible] = useState(false); const [isAddingAccount, setIsAddingAccount] = useState(false); const [selectedBank, setSelectedBank] = useState(null); const [accountNumberInput, setAccountNumberInput] = useState(""); const [savingAccount, setSavingAccount] = useState(false); const [pendingDefaultAccountId, setPendingDefaultAccountId] = useState< string | null >(null); const [toastVisible, setToastVisible] = useState(false); const [toastTitle, setToastTitle] = useState(""); const [toastDescription, setToastDescription] = useState( undefined ); const [toastVariant, setToastVariant] = useState< "success" | "error" | "warning" | "info" >("info"); const toastTimeoutRef = useRef | null>(null); const showToast = ( title: string, description?: string, variant: "success" | "error" | "warning" | "info" = "info" ) => { if (toastTimeoutRef.current) { clearTimeout(toastTimeoutRef.current); } setToastTitle(title); setToastDescription(description); setToastVariant(variant); setToastVisible(true); toastTimeoutRef.current = setTimeout(() => { setToastVisible(false); toastTimeoutRef.current = null; }, 2500); }; useEffect(() => { return () => { if (toastTimeoutRef.current) { clearTimeout(toastTimeoutRef.current); } }; }, []); // Update local state when profile loads useEffect(() => { if (profile) { setEditedProfile({ fullName: profile.fullName || "", phoneNumber: profile.phoneNumber || "", email: profile.email || "", address: profile.address || "", }); // Only sync image from profile if user hasn't picked a local file setProfileImage((prev) => { if (prev && prev.startsWith("file:")) { return prev; } return profile.photoUrl || null; }); const linkedFromProfile: any = (profile as any).linkedAccounts; if (Array.isArray(linkedFromProfile)) { const mapped: ProfileLinkedAccount[] = linkedFromProfile.map( (acc: any) => ({ id: String(acc.id), bankId: String(acc.bankId || ""), bankName: String(acc.bankName || ""), accountNumber: String(acc.accountNumber || ""), isDefault: !!acc.isDefault, }) ); setProfileAccounts(mapped); } else { setProfileAccounts([]); } } }, [profile]); const handleSelectProfileImage = async () => { try { setImagePicking(true); const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (!permissionResult.granted) { Alert.alert( "Permission Required", "Please allow access to your photo library to select a profile picture." ); return; } const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, allowsEditing: true, aspect: [1, 1], quality: 0.8, }); if (!result.canceled && result.assets[0]) { const localUri = result.assets[0].uri; console.log("[EditProfile] Image selected locally", { localUri }); // Only update local preview here. Actual upload happens on Save. setProfileImage(localUri); } } catch (e) { console.error("[EditProfile] Error while selecting profile image", e); showToast("Error", "Failed to select image", "error"); } finally { setImagePicking(false); } }; const handleUpdateProfile = async () => { Keyboard.dismiss(); if (!user?.uid) { showToast( t("profile.toastErrorTitle"), t("profile.toastUserNotFound"), "error" ); return; } // Prepare data for validation based on signup type const validationData: any = { fullName: editedProfile.fullName, address: editedProfile.address || undefined, }; // Only validate phone number if user didn't sign up with phone if (profile?.signupType !== "phone") { validationData.phoneNumber = editedProfile.phoneNumber || undefined; } // Only validate email if user signed up with phone if (profile?.signupType === "phone") { validationData.email = editedProfile.email || undefined; } // Validate form using valibot const validationResult = validate(profileUpdateSchema, validationData); if (!validationResult.success) { showToast("Error", validationResult.error, "warning"); return; } try { setUpdateLoading(true); setUpdateError(null); // If the user picked a new local image (file://), upload it first let finalPhotoUrl: string | undefined = profile?.photoUrl || undefined; if ( profileImage && profileImage !== profile?.photoUrl && profileImage.startsWith("file:") ) { try { console.log("[EditProfile] Uploading profile image on save", { uid: user.uid, localUri: profileImage, }); const uploadedUrl = await uploadProfileImage(user.uid, profileImage); console.log( "[EditProfile] Profile image uploaded on save, url:", uploadedUrl ); finalPhotoUrl = uploadedUrl; setProfileImage(uploadedUrl); } catch (e) { console.error( "[EditProfile] Failed to upload profile image on save", e ); throw new Error( e instanceof Error ? e.message : "Failed to upload profile image" ); } } // Prepare update data based on signup type const updateData: any = { fullName: validationResult.data.fullName, address: validationResult.data.address || undefined, }; if (finalPhotoUrl) { updateData.photoUrl = finalPhotoUrl; } // Only allow phone number update if user didn't sign up with phone if ( profile?.signupType !== "phone" && validationResult.data.phoneNumber ) { updateData.phoneNumber = validationResult.data.phoneNumber; } // Only allow email update if user signed up with phone if (profile?.signupType === "phone" && validationResult.data.email) { updateData.email = validationResult.data.email; } const result = await AuthService.updateUserProfile(user.uid, updateData); if (!result.success) { throw new Error(result.error || "Failed to update profile"); } showToast( t("profile.toastProfileUpdatedTitle"), t("profile.toastProfileUpdatedDescription"), "success" ); // Navigate back to profile after successful update setTimeout(() => { router.back(); }, 1000); } catch (error) { setUpdateError( error instanceof Error ? error.message : "Failed to update profile" ); showToast( t("profile.toastErrorTitle"), t("profile.toastUpdateErrorDescription"), "error" ); } finally { setUpdateLoading(false); } }; const handleCancel = () => { router.back(); }; const handleCopyUsername = async () => { if ( !usernameDisplay || usernameDisplay === t("profile.usernamePlaceholder") ) { showToast("Error", "No username to copy", "error"); return; } await Clipboard.setStringAsync(usernameDisplay); showToast("Copied", "Username copied to clipboard", "success"); }; const handleStartAddAccount = () => { if (profileAccounts.length >= 5) { showToast("Limit reached", "You can link up to 5 accounts.", "warning"); return; } setIsSelectAccountSheetVisible(false); setSelectedBank(null); setAccountNumberInput(""); setIsAddingAccount(true); }; const handleSelectBank = (bankId: string) => { setSelectedBank(bankId); }; const handleSaveAccount = async () => { if (!selectedBank || !accountNumberInput.trim()) { return; } if (profileAccounts.length >= 5) { showToast("Limit reached", "You can link up to 5 accounts.", "warning"); return; } const bank = PROFILE_BANK_OPTIONS.find((b) => b.id === selectedBank); if (!bank) return; const newAccount: ProfileLinkedAccount = { id: `${selectedBank}-${Date.now()}`, bankId: selectedBank, bankName: bank.name, accountNumber: accountNumberInput.trim(), isDefault: profileAccounts.length === 0, }; const updatedAccounts = [...profileAccounts, newAccount]; setProfileAccounts(updatedAccounts); if (!user?.uid) { showToast( t("profile.toastErrorTitle"), t("profile.toastUserNotFound"), "error" ); return; } try { setSavingAccount(true); const result = await AuthService.updateUserProfile(user.uid, { linkedAccounts: updatedAccounts, }); if (!result.success) { throw new Error(result.error || "Failed to save account"); } } catch (error) { showToast( t("profile.toastErrorTitle"), error instanceof Error ? error.message : "Failed to save account", "error" ); } finally { setSavingAccount(false); } setAccountNumberInput(""); setSelectedBank(null); setIsAddingAccount(false); }; const handleConfirmDefaultAccount = async () => { if (!user?.uid) { showToast( t("profile.toastErrorTitle"), t("profile.toastUserNotFound"), "error" ); return; } if (!pendingDefaultAccountId) { setIsSelectAccountSheetVisible(false); return; } const updatedAccounts = profileAccounts.map((account) => ({ ...account, isDefault: account.id === pendingDefaultAccountId, })); setProfileAccounts(updatedAccounts); try { const result = await AuthService.updateUserProfile(user.uid, { linkedAccounts: updatedAccounts, }); if (!result.success) { throw new Error(result.error || "Failed to update default account"); } } catch (error) { showToast( t("profile.toastErrorTitle"), error instanceof Error ? error.message : "Failed to update default account", "error" ); } finally { setIsSelectAccountSheetVisible(false); setPendingDefaultAccountId(null); } }; // Show update error useEffect(() => { if (updateError) { showToast(t("profile.toastUpdateErrorTitle"), updateError, "error"); } }, [updateError]); const languageOptions: DropdownOption[] = [ { value: "en", label: t("profile.languageOptionEnglish") }, { value: "am", label: t("profile.languageOptionAmharic") }, { value: "fr", label: t("profile.languageOptionFrench") }, { value: "ti", label: t("profile.languageOptionTigrinya") }, { value: "om", label: t("profile.languageOptionOromo") }, ]; const isTelecomWallet = selectedBank === "telebirr" || selectedBank === "safaricom"; const accountLabel = isTelecomWallet ? "Phone Number" : "Account Number"; const accountPlaceholder = isTelecomWallet ? "Enter phone number" : "Enter account number"; const defaultAccount = profileAccounts.find((account) => account.isDefault) || null; const usernameDisplay = profile?.fullName ? `@${profile.fullName.split(" ")[0]}` : user?.email ? `@${user.email.split("@")[0]}` : t("profile.usernamePlaceholder"); return ( Edit Profile {profileImage ? ( ) : ( )} {(updateLoading || imagePicking) && ( )} {profileLoading ? t("profile.loadingProfile") : ""} {profileError && ( {t("profile.errorWithMessage", { error: profileError })} )} setEditedProfile((prev) => ({ ...prev, fullName: text })) } containerClassName="w-full" borderClassName="border-[#D9DBE9] bg-white" placeholderColor="#7E7E7E" textClassName="text-[#000] text-sm" /> setEditedProfile((prev) => ({ ...prev, address: text })) } containerClassName="w-full" borderClassName="border-[#D9DBE9] bg-white" placeholderColor="#7E7E7E" textClassName="text-[#000] text-sm" /> setEditedProfile((prev) => ({ ...prev, phoneNumber: text })) } editable={profile?.signupType !== "phone"} containerClassName="w-full" borderClassName="border-[#D9DBE9] bg-white" placeholderColor="#7E7E7E" textClassName="text-[#000] text-sm" keyboardType="phone-pad" /> setEditedProfile((prev) => ({ ...prev, email: text })) } editable={profile?.signupType === "phone"} containerClassName="w-full" borderClassName="border-[#D9DBE9] bg-white" placeholderColor="#7E7E7E" textClassName="text-[#000] text-sm" /> {/* Language Dropdown */} setLanguage(value as any)} placeholder={t("profile.languagePlaceholder")} /> {/* Account Number (read-only) */} { setPendingDefaultAccountId(defaultAccount?.id || null); setIsSelectAccountSheetVisible(true); }} className={`flex-row items-center justify-between ${ defaultAccount ? "py-1 px-4" : "py-3 px-4" } rounded-md border border-[#D9DBE9]`} > {defaultAccount ? defaultAccount.bankName : "Choose account"} {defaultAccount && ( {defaultAccount.accountNumber} )} {/* {profileAccounts.length > 0 && ( Tap to change default account or add another. )} */} {/* Username (read-only) with copy icon */} setIsSelectAccountSheetVisible(false)} maxHeightRatio={0.9} > Choose Account {profileAccounts.length === 0 ? ( You have not added any accounts yet. ) : ( {profileAccounts.map((account) => ( { setPendingDefaultAccountId(account.id); }} className={`flex-row items-center justify-between py-1 px-4 rounded-md mb-2 border ${ ( pendingDefaultAccountId ? pendingDefaultAccountId === account.id : account.isDefault ) ? "border-primary bg-primary/5" : "border-[#D9DBE9] bg-white" }`} > {account.bankName} {account.accountNumber} {account.isDefault && ( Default )} ))} )} {profileAccounts.length > 0 && ( )} {profileAccounts.length >= 5 && ( You can link up to 5 accounts. )} setIsAddingAccount(false)} maxHeightRatio={0.9} > Add Account Bank {PROFILE_BANK_OPTIONS.map((bank) => { const isSelected = selectedBank === bank.id; const initials = bank.name .split(" ") .map((part) => part[0]) .join("") .toUpperCase() .slice(0, 2); return ( handleSelectBank(bank.id)} className={`items-center justify-between px-3 py-4 mb-3 rounded-2xl border ${ isSelected ? "border-primary bg-primary/5" : "border-gray-200 bg-white" }`} style={{ width: "30%" }} > {initials} {bank.name} ); })} {accountLabel} setAccountNumberInput(text.replace(/[^0-9]/g, "")) } containerClassName="w-full mb-4" borderClassName="border-[#E5E7EB] bg-white rounded-[4px]" placeholderColor="#9CA3AF" textClassName="text-[#111827] text-sm" keyboardType="number-pad" /> ); }