Amba-Agent-App/lib/stores/contactsStore.ts
2026-01-16 00:22:35 +03:00

255 lines
7.3 KiB
TypeScript

/**
* Contacts Store - Platform-aware contact management
*
* Native (Android/iOS): Uses expo-contacts to access device contacts
* Web: Gracefully degrades - contacts feature not available
*/
import { create } from "zustand";
import { Platform, Alert } from "react-native";
import { formatPhoneNumber, isValidPhoneNumber } from "../utils/phoneUtils";
import { awardFirstContactSyncPoints } from "../services/pointsService";
// Only import expo-contacts on native platforms
let Contacts: any = null;
if (Platform.OS !== "web") {
Contacts = require("expo-contacts");
}
export interface Contact {
id: string;
name: string;
firstName?: string;
lastName?: string;
phoneNumbers?: Array<{
number: string;
formattedNumber: string;
isPrimary?: boolean;
label?: string;
}>;
emails?: Array<{
email: string;
isPrimary?: boolean;
label?: string;
}>;
imageAvailable?: boolean;
image?: any;
}
interface ContactsState {
// State
contacts: Contact[];
loading: boolean;
error: string | null;
hasPermission: boolean;
isInitialized: boolean;
isSupported: boolean; // New: indicates if contacts are supported on this platform
// Actions
setContacts: (contacts: Contact[]) => void;
setLoading: (loading: boolean) => void;
setError: (error: string | null) => void;
setHasPermission: (hasPermission: boolean) => void;
setIsInitialized: (isInitialized: boolean) => void;
// Permission actions
checkPermission: () => Promise<boolean>;
requestPermission: () => Promise<void>;
// Data actions
loadContacts: () => Promise<void>;
refreshContacts: () => Promise<void>;
findContactByPhoneNumber: (phoneNumber: string) => Contact | null;
// Initialize
initialize: () => Promise<void>;
}
export const useContactsStore = create<ContactsState>((set, get) => ({
// Initial state
contacts: [],
loading: false,
error: null,
hasPermission: false,
isInitialized: false,
isSupported: Platform.OS !== "web", // Contacts not supported on web
// Setters
setContacts: (contacts) => set({ contacts }),
setLoading: (loading) => set({ loading }),
setError: (error) => set({ error }),
setHasPermission: (hasPermission) => set({ hasPermission }),
setIsInitialized: (isInitialized) => set({ isInitialized }),
// Check permission status
checkPermission: async (): Promise<boolean> => {
// Web: No permission needed (feature not available)
if (Platform.OS === "web" || !Contacts) {
set({ hasPermission: false, isSupported: false });
return false;
}
try {
const { status } = await Contacts.getPermissionsAsync();
const granted = status === "granted";
set({ hasPermission: granted });
return granted;
} catch (err) {
console.error("Error checking contacts permission:", err);
set({ error: "Failed to check contacts permission" });
return false;
}
},
// Request permission
requestPermission: async (): Promise<void> => {
// Web: Show message that contacts aren't supported
if (Platform.OS === "web" || !Contacts) {
set({ hasPermission: false, isSupported: false });
console.log("Contacts not supported on web platform");
return;
}
try {
set({ error: null });
const { status } = await Contacts.requestPermissionsAsync();
if (status === "granted") {
set({ hasPermission: true });
await get().loadContacts();
} else {
set({ hasPermission: false, error: "Contacts permission denied" });
Alert.alert(
"Permission Required",
"To show your contacts as potential recipients, please grant contacts permission in your device settings.",
[{ text: "OK", style: "default" }]
);
}
} catch (err) {
console.error("Error requesting contacts permission:", err);
set({ error: "Failed to request contacts permission" });
}
},
// Load contacts from device
loadContacts: async (): Promise<void> => {
// Web: Skip loading
if (Platform.OS === "web" || !Contacts) {
set({ loading: false, contacts: [], isSupported: false });
return;
}
try {
set({ loading: true, error: null });
const { data } = await Contacts.getContactsAsync({
fields: [
Contacts.Fields.ID,
Contacts.Fields.Name,
Contacts.Fields.FirstName,
Contacts.Fields.LastName,
Contacts.Fields.PhoneNumbers,
Contacts.Fields.Emails,
Contacts.Fields.ImageAvailable,
Contacts.Fields.Image,
],
sort: Contacts.SortTypes.FirstName,
});
// Filter and format contacts
const formattedContacts: Contact[] = data
.filter((contact: any) => {
return (
contact.name &&
contact.phoneNumbers &&
contact.phoneNumbers.length > 0 &&
contact.phoneNumbers.some(
(phone: any) => phone.number && isValidPhoneNumber(phone.number)
)
);
})
.map((contact: any) => ({
id: contact.id || "",
name: contact.name || "",
firstName: contact.firstName,
lastName: contact.lastName,
phoneNumbers: contact.phoneNumbers
?.filter(
(phone: any) => phone.number && isValidPhoneNumber(phone.number)
)
?.map((phone: any) => ({
number: formatPhoneNumber(phone.number || ""),
formattedNumber: formatPhoneNumber(phone.number || ""),
isPrimary: phone.isPrimary,
label: phone.label,
})),
emails: contact.emails?.map((email: any) => ({
email: email.email || "",
isPrimary: email.isPrimary,
label: email.label,
})),
imageAvailable: contact.imageAvailable,
image: contact.image,
}));
set({ contacts: formattedContacts });
console.log(`Loaded ${formattedContacts.length} contacts`);
try {
await awardFirstContactSyncPoints();
} catch (error) {
console.warn(
"[ContactsStore] Failed to award contact sync points",
error
);
}
} catch (err) {
console.error("Error loading contacts:", err);
set({ error: "Failed to load contacts", contacts: [] });
} finally {
set({ loading: false });
}
},
// Refresh contacts
refreshContacts: async (): Promise<void> => {
if (Platform.OS === "web") {
return;
}
const permitted = await get().checkPermission();
if (permitted) {
await get().loadContacts();
}
},
// Find contact by phone number
findContactByPhoneNumber: (phoneNumber: string): Contact | null => {
const { contacts } = get();
const formattedNumber = formatPhoneNumber(phoneNumber);
return (
contacts.find((contact) =>
contact.phoneNumbers?.some(
(phone) => phone.formattedNumber === formattedNumber
)
) || null
);
},
// Initialize on mount
initialize: async () => {
// Web: Mark as initialized but skip permission/loading
if (Platform.OS === "web") {
set({ isInitialized: true, isSupported: false });
return;
}
const permitted = await get().checkPermission();
if (permitted) {
await get().loadContacts();
}
set({ isInitialized: true });
},
}));