457 lines
16 KiB
TypeScript
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>
|
|
);
|
|
}
|