/** * 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; requestPermission: () => Promise; // Data actions loadContacts: () => Promise; refreshContacts: () => Promise; findContactByPhoneNumber: (phoneNumber: string) => Contact | null; // Initialize initialize: () => Promise; } export const useContactsStore = create((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 => { // 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 => { // 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 => { // 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 => { 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 }); }, }));