import { notifyHabitPetDataUpdated } from "@/lib/app-events"; import { normalizeReminderDeliveryMethod, type ReminderDeliveryMethod, } from "@/lib/reminder-delivery"; import { createClient } from "@/lib/supabase/client"; export type ProfilePreferences = { focusTopic: string; avatarVibe: string; dailyReminderEnabled: boolean; dailyReminderTime: string; dailyReminderDelivery: ReminderDeliveryMethod; }; type StoredProfilePreferences = { focusTopic?: string; avatarVibe?: string; dailyReminderEnabled?: boolean; dailyReminderTime?: string; dailyReminderDelivery?: string; }; type ProfileRow = { focus_topics: string | string[] | null; onboarding_complete: boolean | null; }; export const focusTopicOptions = [ "Sleep", "Movement", "Hydration", "Mindfulness", ] as const; export const avatarVibeOptions = ["Calm", "Energetic", "Curious", "Cozy"] as const; export const defaultProfilePreferences: ProfilePreferences = { focusTopic: "Movement", avatarVibe: "Calm", dailyReminderEnabled: false, dailyReminderTime: "20:00", dailyReminderDelivery: "in_app", }; function isFocusTopic(value: string) { return focusTopicOptions.includes(value as (typeof focusTopicOptions)[number]); } function isAvatarVibe(value: string) { return avatarVibeOptions.includes(value as (typeof avatarVibeOptions)[number]); } function normalizeFocusTopic(value: string | undefined) { if (value && isFocusTopic(value)) { return value; } return defaultProfilePreferences.focusTopic; } function normalizeAvatarVibe(value: string | undefined) { if (value && isAvatarVibe(value)) { return value; } return defaultProfilePreferences.avatarVibe; } function normalizePreferences( partial: Partial, ): ProfilePreferences { return { focusTopic: normalizeFocusTopic(partial.focusTopic), avatarVibe: normalizeAvatarVibe(partial.avatarVibe), dailyReminderEnabled: partial.dailyReminderEnabled ?? defaultProfilePreferences.dailyReminderEnabled, dailyReminderTime: partial.dailyReminderTime ?? defaultProfilePreferences.dailyReminderTime, dailyReminderDelivery: normalizeReminderDeliveryMethod( partial.dailyReminderDelivery, ), }; } function parseStoredPreferencesJson(raw: string): Partial { try { const parsed = JSON.parse(raw) as StoredProfilePreferences; if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { return parsed as Partial; } } catch { return { focusTopic: raw }; } return { focusTopic: raw }; } function parseFocusTopicsField( value: string | string[] | null | undefined, ): Partial { if (Array.isArray(value)) { if (value.length === 0) { return {}; } return parseStoredPreferencesJson(value[0] ?? ""); } if (!value?.trim()) { return {}; } return parseStoredPreferencesJson(value); } function serializeFocusTopicsField(preferences: ProfilePreferences) { const payload: StoredProfilePreferences = { focusTopic: preferences.focusTopic, avatarVibe: preferences.avatarVibe, dailyReminderEnabled: preferences.dailyReminderEnabled, dailyReminderTime: preferences.dailyReminderTime, dailyReminderDelivery: preferences.dailyReminderDelivery, }; return JSON.stringify(payload); } function isFocusTopicsTypeError(message: string) { return /array|malformed|invalid input syntax/i.test(message); } function mapRowToPreferences(row: ProfileRow): ProfilePreferences { return normalizePreferences(parseFocusTopicsField(row.focus_topics)); } function hasStoredPreferences(row: ProfileRow) { if (Array.isArray(row.focus_topics)) { return row.focus_topics.some((value) => value.trim().length > 0); } return Boolean(row.focus_topics?.trim()); } async function getAuthenticatedUserId() { const supabase = createClient(); const { data: { user }, error, } = await supabase.auth.getUser(); if (error || !user) { return null; } return user.id; } function getOnboardingAnswersFromMetadata( metadata: Record | undefined, ) { const answers = metadata?.onboarding_answers; if (!answers || typeof answers !== "object") { return null; } const record = answers as Record; return { focusTopic: record.focus, avatarVibe: record.avatarVibe ?? record["pet-name"], }; } async function saveProfileRow( userId: string, preferences: ProfilePreferences, options?: { markOnboardingComplete?: boolean; markQuizComplete?: boolean; }, ) { const supabase = createClient(); const serialized = serializeFocusTopicsField(preferences); const focusTopicAttempts: Array = [ serialized, [serialized], ]; const onboardingFields = { ...(options?.markOnboardingComplete ? { onboarding_complete: true } : {}), ...(options?.markQuizComplete ? { onboarding_quiz_complete: true } : {}), }; let lastError: string | null = null; for (const focus_topics of focusTopicAttempts) { const { data: updatedRows, error: updateError } = await supabase .from("profiles") .update({ focus_topics, ...onboardingFields, }) .eq("id", userId) .select("focus_topics, onboarding_complete"); if (!updateError && updatedRows && updatedRows.length > 0) { return updatedRows[0] as ProfileRow; } if (updateError) { lastError = updateError.message; if (!isFocusTopicsTypeError(updateError.message)) { throw new Error(updateError.message); } } } for (const focus_topics of focusTopicAttempts) { const { data: insertedRow, error: insertError } = await supabase .from("profiles") .insert({ id: userId, focus_topics, ...onboardingFields, }) .select("focus_topics, onboarding_complete") .single(); if (!insertError && insertedRow) { return insertedRow as ProfileRow; } if (insertError) { lastError = insertError.message; if (!isFocusTopicsTypeError(insertError.message)) { throw new Error(insertError.message); } } } throw new Error(lastError ?? "Could not save profile preferences."); } async function syncPreferencesFromMetadata(userId: string) { const supabase = createClient(); const { data: { user }, } = await supabase.auth.getUser(); const onboardingAnswers = getOnboardingAnswersFromMetadata(user?.user_metadata); if (!onboardingAnswers?.focusTopic && !onboardingAnswers?.avatarVibe) { return null; } const preferences = normalizePreferences(onboardingAnswers); const row = await saveProfileRow(userId, preferences, { markOnboardingComplete: true, }); return mapRowToPreferences(row); } export async function getProfilePreferences(): Promise { const userId = await getAuthenticatedUserId(); if (!userId) { return defaultProfilePreferences; } const supabase = createClient(); const { data, error } = await supabase .from("profiles") .select("focus_topics, onboarding_complete") .eq("id", userId) .maybeSingle(); if (error) { throw new Error(error.message); } if (!data) { return defaultProfilePreferences; } const row = data as ProfileRow; if (!hasStoredPreferences(row)) { const synced = await syncPreferencesFromMetadata(userId); return synced ?? mapRowToPreferences(row); } return mapRowToPreferences(row); } export async function saveProfilePreferences(preferences: ProfilePreferences) { const userId = await getAuthenticatedUserId(); if (!userId) { throw new Error("You must be signed in to save profile preferences."); } const normalizedPreferences = normalizePreferences(preferences); await saveProfileRow(userId, normalizedPreferences); notifyHabitPetDataUpdated(); return normalizedPreferences; } export async function saveOnboardingPreferences(focusTopic: string) { const userId = await getAuthenticatedUserId(); if (!userId) { throw new Error("You must be signed in to save onboarding preferences."); } const supabase = createClient(); const preferences = normalizePreferences({ focusTopic }); await saveProfileRow(userId, preferences, { markQuizComplete: true }); const { error: authError } = await supabase.auth.updateUser({ data: { onboarding_quiz_completed: true, onboarding_answers: { focus: preferences.focusTopic, }, }, }); if (authError) { throw new Error(authError.message); } notifyHabitPetDataUpdated(); return preferences; }