Amba-Agent-App/components/ui/contactModal.tsx
2026-01-16 00:22:35 +03:00

457 lines
16 KiB
TypeScript

import React, { useState, useEffect } from "react";
import {
Modal,
View,
Text,
Image,
TouchableOpacity,
ScrollView,
Linking,
} from "react-native";
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
import { SafeAreaView } from "react-native-safe-area-context";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { X, Phone, Mail, User, MessageCircle } from "lucide-react-native";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import BottomSheet from "~/components/ui/bottomSheet";
import { Contact } from "~/lib/stores";
import { router } from "expo-router";
import { ROUTES } from "~/lib/routes";
import { useTranslation } from "react-i18next";
interface ContactModalProps {
visible: boolean;
contact: Contact | null;
onClose: () => void;
}
type LinkedAccount = {
id: string;
bankId: string;
bankName: string;
accountNumber: string;
};
const 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 ContactModal({
visible,
contact,
onClose,
}: ContactModalProps) {
const { t } = useTranslation();
const [linkedAccounts, setLinkedAccounts] = useState<LinkedAccount[]>([]);
const [isAddingAccount, setIsAddingAccount] = useState(false);
const [selectedBank, setSelectedBank] = useState<string | null>(null);
const [accountNumber, setAccountNumber] = useState("");
useEffect(() => {
const loadLinkedAccounts = async () => {
if (!visible || !contact?.id) {
setLinkedAccounts([]);
return;
}
try {
const storageKey = `contact_linked_accounts_${contact.id}`;
const stored = await AsyncStorage.getItem(storageKey);
if (stored) {
const parsed = JSON.parse(stored);
if (Array.isArray(parsed)) {
setLinkedAccounts(parsed as LinkedAccount[]);
} else {
setLinkedAccounts([]);
}
} else {
setLinkedAccounts([]);
}
} catch (error) {
if (__DEV__) {
console.warn(
"[ContactModal] Failed to load linked accounts from storage",
error
);
}
setLinkedAccounts([]);
}
};
loadLinkedAccounts();
}, [visible, contact]);
if (!contact) return null;
const displayName =
contact.name || t("components.contactmodal.unknownContact");
const initials = displayName
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
.substring(0, 2);
const handleCall = (phoneNumber: string) => {
const cleanNumber = phoneNumber.replace(/[^+\d]/g, "");
Linking.openURL(`tel:${cleanNumber}`);
};
const handleSMS = (phoneNumber: string) => {
const cleanNumber = phoneNumber.replace(/[^+\d]/g, "");
Linking.openURL(`sms:${cleanNumber}`);
};
const handleEmail = (email: string) => {
Linking.openURL(`mailto:${email}`);
};
const handleSendMoney = () => {
if (!contact) return;
// Close modal first
onClose();
// Navigate to send money with contact info
router.push({
pathname: ROUTES.SEND_OR_REQUEST_MONEY,
params: {
selectedContactId: contact.id,
selectedContactName: contact.name,
selectedContactPhone: contact.phoneNumbers?.[0]?.number || "",
},
});
};
const handleStartAddAccount = () => {
setIsAddingAccount(true);
};
const handleSelectBank = (bankId: string) => {
setSelectedBank(bankId);
};
const handleSaveAccount = async () => {
if (!selectedBank || !accountNumber.trim()) {
return;
}
const bank = BANK_OPTIONS.find((b) => b.id === selectedBank);
if (!bank) return;
if (!contact?.id) {
return;
}
const newAccount: LinkedAccount = {
id: `${selectedBank}-${Date.now()}`,
bankId: selectedBank,
bankName: bank.name,
accountNumber: accountNumber.trim(),
};
const updatedAccounts = [...linkedAccounts, newAccount];
setLinkedAccounts(updatedAccounts);
try {
const storageKey = `contact_linked_accounts_${contact.id}`;
await AsyncStorage.setItem(storageKey, JSON.stringify(updatedAccounts));
} catch (error) {
if (__DEV__) {
console.warn(
"[ContactModal] Failed to persist linked accounts to storage",
error
);
}
}
setAccountNumber("");
setSelectedBank(null);
setIsAddingAccount(false);
};
const isTelecomWallet =
selectedBank === "telebirr" || selectedBank === "safaricom";
const accountLabel = isTelecomWallet ? "Phone Number" : "Account Number";
const accountPlaceholder = isTelecomWallet
? "Enter phone number"
: "Enter account number";
return (
<Modal
visible={visible}
animationType="slide"
presentationStyle="pageSheet"
onRequestClose={onClose}
>
<SafeAreaView className="flex-1 bg-white">
<Animated.View
className="flex-1"
entering={FadeIn.duration(200).delay(50)}
exiting={FadeOut.duration(150)}
>
{/* Header */}
<View className="flex flex-row items-center justify-between py-3 border-b border-gray-200">
<Text className="text-lg font-dmsans-bold px-4 text-primary">
{t("components.contactmodal.headerTitle")}
</Text>
<TouchableOpacity onPress={onClose} className="px-4 py-2">
<X className="text-gray-500" size={24} />
</TouchableOpacity>
</View>
<ScrollView className="flex-1">
{/* Contact Avatar & Name */}
<View className="flex items-center py-8 px-5">
<View className="w-24 h-24 rounded-full bg-primary/10 flex items-center justify-center mb-4">
{contact.imageAvailable && contact.image ? (
<Image
source={{ uri: contact.image.uri }}
className="w-24 h-24 rounded-full"
resizeMode="cover"
/>
) : (
<Text className="text-primary font-dmsans-bold text-2xl">
{initials}
</Text>
)}
</View>
<Text className="text-2xl font-dmsans-bold text-primary text-center mb-2">
{displayName}
</Text>
{contact.firstName && contact.lastName && (
<Text className="text-gray-500 font-dmsans text-base text-center">
{contact.firstName} {contact.lastName}
</Text>
)}
</View>
{/* Contact Information */}
<View className="px-5 space-y-6">
{/* Phone Numbers */}
{contact.phoneNumbers && contact.phoneNumbers.length > 0 && (
<View>
<Text className="text-lg font-dmsans-bold text-gray-800 mb-3">
{t("components.contactmodal.phoneNumbersTitle")}
</Text>
{contact.phoneNumbers.map((phone, index) => (
<View
key={index}
className="flex flex-row items-center justify-between py-3 px-4 bg-gray-50 rounded-lg mb-2"
>
<View className="flex flex-row items-center flex-1">
<Phone className="text-primary mr-3" size={20} />
{/* Add horizontal spacer here */}
<View className="w-2" />
<View className="flex-1">
<Text className="text-base font-dmsans text-gray-800">
{phone.number}
</Text>
{phone.label && (
<Text className="text-sm font-dmsans text-gray-500">
{phone.label}
</Text>
)}
</View>
</View>
{/* <View className="flex flex-row space-x-2">
<TouchableOpacity
onPress={() => handleCall(phone.number)}
className="p-2 bg-green-100 rounded-full"
>
<Phone className="text-green-600" size={16} />
</TouchableOpacity>
<TouchableOpacity
onPress={() => handleSMS(phone.number)}
className="p-2 bg-blue-100 rounded-full"
>
<MessageCircle className="text-blue-600" size={16} />
</TouchableOpacity>
</View> */}
</View>
))}
</View>
)}
{/* Linked Accounts */}
{linkedAccounts.length > 0 && (
<View className="mt-6">
<Text className="text-lg font-dmsans-bold text-gray-800 mb-3">
Linked Accounts
</Text>
{linkedAccounts.map((account) => (
<View
key={account.id}
className="flex flex-row items-center justify-between py-3 px-4 bg-gray-50 rounded-lg mb-2"
>
<View className="flex-1 mr-3">
<Text className="text-base font-dmsans text-gray-800">
{account.bankName}
</Text>
<Text className="text-sm font-dmsans text-gray-500 mt-1">
{account.accountNumber}
</Text>
</View>
</View>
))}
</View>
)}
{/* Email Addresses */}
{contact.emails && contact.emails.length > 0 && (
<View>
<Text className="text-lg font-dmsans-bold text-gray-800 mb-3">
{t("components.contactmodal.emailAddressesTitle")}
</Text>
{contact.emails.map((email, index) => (
<View
key={index}
className="flex flex-row items-center justify-between py-3 px-4 bg-gray-50 rounded-lg mb-2"
>
<View className="flex flex-row items-center flex-1">
<Mail className="text-primary mr-3" size={20} />
<View className="w-2" />
<View className="flex-1">
<Text className="text-base font-dmsans text-gray-800">
{email.email}
</Text>
{email.label && (
<Text className="text-sm font-dmsans text-gray-500">
{email.label}
</Text>
)}
</View>
</View>
{/* <TouchableOpacity
onPress={() => handleEmail(email.email)}
className="p-2 bg-purple-100 rounded-full"
>
<Mail className="text-purple-600" size={16} />
</TouchableOpacity> */}
</View>
))}
</View>
)}
{/* No additional info message */}
{(!contact.phoneNumbers || contact.phoneNumbers.length === 0) &&
(!contact.emails || contact.emails.length === 0) && (
<View className="flex items-center py-8">
<User className="text-gray-400 mb-3" size={48} />
<Text className="text-gray-500 font-dmsans text-center">
{t("components.contactmodal.noAdditionalInfo")}
</Text>
</View>
)}
</View>
</ScrollView>
{/* Action Buttons */}
<View className="px-5 py-4 border-t border-gray-200 space-y-3">
<Button className="bg-primary rounded-md" onPress={handleSendMoney}>
<Text className="font-dmsans text-white">
{t("components.contactmodal.sendMoneyButton")}
</Text>
</Button>
<View className="h-1" />
<Button
className="bg-[#FFB668] rounded-md"
onPress={handleStartAddAccount}
>
<Text className="font-dmsans text-white">Add Account</Text>
</Button>
</View>
</Animated.View>
</SafeAreaView>
{/* Add Account Bottom Sheet */}
<BottomSheet
visible={isAddingAccount}
onClose={() => setIsAddingAccount(false)}
maxHeightRatio={0.9}
>
<View className="mb-4">
<Text className="text-xl font-dmsans-bold text-primary text-center">
Add Account
</Text>
</View>
<View className="mb-4">
<Text className="text-base font-dmsans text-black mb-2">Bank</Text>
<View className="flex-row flex-wrap justify-between">
{BANK_OPTIONS.map((bank) => {
const isSelected = selectedBank === bank.id;
const initials = bank.name
.split(" ")
.map((part) => part[0])
.join("")
.toUpperCase()
.slice(0, 2);
return (
<TouchableOpacity
key={bank.id}
activeOpacity={0.8}
onPress={() => 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%" }}
>
<View className="w-10 h-10 mb-2 rounded-full bg-primary/10 items-center justify-center">
<Text className="text-primary font-dmsans-bold text-sm">
{initials}
</Text>
</View>
<Text
className="text-center text-xs font-dmsans text-gray-800"
numberOfLines={2}
>
{bank.name}
</Text>
</TouchableOpacity>
);
})}
</View>
</View>
<View className="mb-4">
<Text className="text-base font-dmsans text-black mb-2">
{accountLabel}
</Text>
<Input
placeholderText={accountPlaceholder}
value={accountNumber}
onChangeText={(text) =>
setAccountNumber(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"
/>
</View>
<Button
className="bg-primary rounded-3xl w-full"
onPress={handleSaveAccount}
disabled={!selectedBank || !accountNumber.trim()}
>
<Text className="font-dmsans text-white">Save Account</Text>
</Button>
</BottomSheet>
</Modal>
);
}