255 lines
7.3 KiB
TypeScript
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 });
|
|
},
|
|
}));
|