export type ThemeMode = 'dark' | 'light'; export type InterfaceLanguage = 'english' | 'spanish' | 'french' | 'german'; export type FontSizePreference = 'small' | 'medium' | 'large'; export type DeliveryAddress = { fullName: string; phone: string; street: string; city: string; state: string; pincode: string; country: string; }; export type UserPreferences = { themeMode: ThemeMode; language: InterfaceLanguage; fontSize: FontSizePreference; emailNotifications: boolean; pushNotifications: boolean; soundEnabled: boolean; autoSave: boolean; twoFactorEnabled: boolean; twoFactorCode: string; dataAnalytics: boolean; address: DeliveryAddress; updatedAt: string; }; type ImportedPreferencesPayload = Partial & { version?: number; exportedAt?: string; }; export type AnalyticsEvent = { type: string; createdAt: string; payload?: Record; }; const PREFERENCES_STORAGE_PREFIX = 'ryp_preferences'; const ANALYTICS_STORAGE_PREFIX = 'ryp_analytics'; const LEGACY_ADDRESS_KEY = 'cq_delivery_address'; const LEGACY_KEYS = { emailNotifications: 'emailNotifications', pushNotifications: 'pushNotifications', soundEnabled: 'soundEnabled', autoSave: 'autoSave', twoFactorEnabled: 'twoFactorEnabled', dataAnalytics: 'dataAnalytics', language: 'language', fontSize: 'fontSize', themeMode: 'theme_mode', } as const; export const SETTINGS_EXPORT_VERSION = 1; export const EMPTY_ADDRESS: DeliveryAddress = { fullName: '', phone: '', street: '', city: '', state: '', pincode: '', country: 'India', }; export function createDefaultPreferences(themeMode: ThemeMode = 'dark'): UserPreferences { return { themeMode, language: 'english', fontSize: 'medium', emailNotifications: true, pushNotifications: true, soundEnabled: true, autoSave: true, twoFactorEnabled: false, twoFactorCode: '', dataAnalytics: true, address: { ...EMPTY_ADDRESS }, updatedAt: new Date().toISOString(), }; } function preferencesStorageKey(userId: string) { return `${PREFERENCES_STORAGE_PREFIX}_${userId}`; } function analyticsStorageKey(userId: string) { return `${ANALYTICS_STORAGE_PREFIX}_${userId}`; } export function userScopedStorageKey(scope: string, userId: string) { return `ryp_${scope}_${userId}`; } function isPlainObject(value: unknown): value is Record { return Boolean(value) && typeof value === 'object' && !Array.isArray(value); } function normalizeBoolean(value: unknown, fallback: boolean) { return typeof value === 'boolean' ? value : fallback; } function normalizeThemeMode(value: unknown, fallback: ThemeMode): ThemeMode { return value === 'light' || value === 'dark' ? value : fallback; } function normalizeLanguage(value: unknown, fallback: InterfaceLanguage): InterfaceLanguage { return value === 'english' || value === 'spanish' || value === 'french' || value === 'german' ? value : fallback; } function normalizeFontSize(value: unknown, fallback: FontSizePreference): FontSizePreference { return value === 'small' || value === 'medium' || value === 'large' ? value : fallback; } export function normalizeAddress(value: unknown): DeliveryAddress { if (!isPlainObject(value)) { return { ...EMPTY_ADDRESS }; } return { fullName: typeof value.fullName === 'string' ? value.fullName : '', phone: typeof value.phone === 'string' ? value.phone : '', street: typeof value.street === 'string' ? value.street : '', city: typeof value.city === 'string' ? value.city : '', state: typeof value.state === 'string' ? value.state : '', pincode: typeof value.pincode === 'string' ? value.pincode : '', country: typeof value.country === 'string' && value.country.trim() ? value.country : EMPTY_ADDRESS.country, }; } export function normalizePreferences( value: unknown, fallbackThemeMode: ThemeMode = 'dark', ): UserPreferences { const defaults = createDefaultPreferences(fallbackThemeMode); if (!isPlainObject(value)) { return defaults; } const twoFactorEnabled = normalizeBoolean(value.twoFactorEnabled, defaults.twoFactorEnabled); const twoFactorCode = typeof value.twoFactorCode === 'string' ? value.twoFactorCode.trim() : ''; return { themeMode: normalizeThemeMode(value.themeMode, defaults.themeMode), language: normalizeLanguage(value.language, defaults.language), fontSize: normalizeFontSize(value.fontSize, defaults.fontSize), emailNotifications: normalizeBoolean(value.emailNotifications, defaults.emailNotifications), pushNotifications: normalizeBoolean(value.pushNotifications, defaults.pushNotifications), soundEnabled: normalizeBoolean(value.soundEnabled, defaults.soundEnabled), autoSave: normalizeBoolean(value.autoSave, defaults.autoSave), twoFactorEnabled: twoFactorEnabled && twoFactorCode.length >= 6, twoFactorCode, dataAnalytics: normalizeBoolean(value.dataAnalytics, defaults.dataAnalytics), address: normalizeAddress(value.address), updatedAt: typeof value.updatedAt === 'string' && value.updatedAt.trim() ? value.updatedAt : defaults.updatedAt, }; } function readJson(key: string): T | null { if (typeof window === 'undefined') { return null; } const raw = window.localStorage.getItem(key); if (!raw) { return null; } try { return JSON.parse(raw) as T; } catch { return null; } } function writeJson(key: string, value: unknown) { if (typeof window === 'undefined') { return; } window.localStorage.setItem(key, JSON.stringify(value)); } function removeValue(key: string) { if (typeof window === 'undefined') { return; } window.localStorage.removeItem(key); } function readLegacyPreferences(themeMode: ThemeMode): UserPreferences { if (typeof window === 'undefined') { return createDefaultPreferences(themeMode); } const defaults = createDefaultPreferences(themeMode); const legacyAddress = normalizeAddress(readJson(LEGACY_ADDRESS_KEY)); return { ...defaults, themeMode: normalizeThemeMode(window.localStorage.getItem(LEGACY_KEYS.themeMode), defaults.themeMode), language: normalizeLanguage(window.localStorage.getItem(LEGACY_KEYS.language), defaults.language), fontSize: normalizeFontSize(window.localStorage.getItem(LEGACY_KEYS.fontSize), defaults.fontSize), emailNotifications: window.localStorage.getItem(LEGACY_KEYS.emailNotifications) !== 'false', pushNotifications: window.localStorage.getItem(LEGACY_KEYS.pushNotifications) !== 'false', soundEnabled: window.localStorage.getItem(LEGACY_KEYS.soundEnabled) !== 'false', autoSave: window.localStorage.getItem(LEGACY_KEYS.autoSave) !== 'false', twoFactorEnabled: window.localStorage.getItem(LEGACY_KEYS.twoFactorEnabled) === 'true', dataAnalytics: window.localStorage.getItem(LEGACY_KEYS.dataAnalytics) !== 'false', address: legacyAddress, }; } function clearLegacySettings() { if (typeof window === 'undefined') { return; } Object.values(LEGACY_KEYS).forEach((key) => { window.localStorage.removeItem(key); }); window.localStorage.removeItem(LEGACY_ADDRESS_KEY); } export function loadUserPreferences(userId: string, fallbackThemeMode: ThemeMode = 'dark') { const stored = readJson(preferencesStorageKey(userId)); if (stored) { return normalizePreferences(stored, fallbackThemeMode); } const migrated = readLegacyPreferences(fallbackThemeMode); saveUserPreferences(userId, migrated); clearLegacySettings(); return migrated; } export function saveUserPreferences(userId: string, preferences: UserPreferences) { const normalized = normalizePreferences(preferences, preferences.themeMode); normalized.updatedAt = new Date().toISOString(); writeJson(preferencesStorageKey(userId), normalized); return normalized; } export function resetUserPreferences(userId: string, themeMode: ThemeMode = 'dark') { const defaults = createDefaultPreferences(themeMode); writeJson(preferencesStorageKey(userId), defaults); return defaults; } export function exportUserPreferences(preferences: UserPreferences) { return { version: SETTINGS_EXPORT_VERSION, exportedAt: new Date().toISOString(), ...preferences, }; } export function parseImportedPreferences( rawText: string, fallbackThemeMode: ThemeMode = 'dark', ) { const parsed = JSON.parse(rawText) as ImportedPreferencesPayload; return normalizePreferences(parsed, fallbackThemeMode); } export function createTwoFactorCode() { return String(Math.floor(100000 + Math.random() * 900000)); } export function fontSizeToRootRem(fontSize: FontSizePreference) { if (fontSize === 'small') { return '14px'; } if (fontSize === 'large') { return '18px'; } return '16px'; } export function getCodeEditorFontSize(fontSize: FontSizePreference) { if (fontSize === 'small') { return 13; } if (fontSize === 'large') { return 17; } return 15; } export function trackAnalyticsEvent( userId: string, enabled: boolean, type: string, payload?: Record, ) { if (!enabled || typeof window === 'undefined') { return; } const current = readJson(analyticsStorageKey(userId)) ?? []; const next = [ { type, payload, createdAt: new Date().toISOString(), }, ...current, ].slice(0, 50); writeJson(analyticsStorageKey(userId), next); } export function loadAnalyticsEvents(userId: string) { return readJson(analyticsStorageKey(userId)) ?? []; } export function clearAnalyticsEvents(userId: string) { removeValue(analyticsStorageKey(userId)); } export function saveUserScopedJson(scope: string, userId: string, value: unknown) { writeJson(userScopedStorageKey(scope, userId), value); } export function loadUserScopedJson(scope: string, userId: string) { return readJson(userScopedStorageKey(scope, userId)); } export function removeUserScopedJson(scope: string, userId: string) { removeValue(userScopedStorageKey(scope, userId)); } export async function requestBrowserNotificationPermission() { if (typeof window === 'undefined' || !('Notification' in window)) { return 'unsupported' as const; } if (Notification.permission === 'granted') { return 'granted' as const; } if (Notification.permission === 'denied') { return 'denied' as const; } return Notification.requestPermission(); } export function sendBrowserNotification( enabled: boolean, title: string, options?: NotificationOptions, ) { if ( !enabled || typeof window === 'undefined' || !('Notification' in window) || Notification.permission !== 'granted' ) { return; } try { const notification = new Notification(title, options); window.setTimeout(() => notification.close(), 5000); } catch { // Ignore browser notification failures. } } export function playUiSound(enabled: boolean, variant: 'success' | 'error' | 'soft' = 'soft') { if (!enabled || typeof window === 'undefined') { return; } const AudioCtor = window.AudioContext || (window as typeof window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext; if (!AudioCtor) { return; } const context = new AudioCtor(); const oscillator = context.createOscillator(); const gain = context.createGain(); const now = context.currentTime; oscillator.type = variant === 'error' ? 'sawtooth' : 'sine'; oscillator.frequency.setValueAtTime(variant === 'error' ? 220 : variant === 'success' ? 660 : 440, now); gain.gain.setValueAtTime(0.0001, now); gain.gain.exponentialRampToValueAtTime(0.05, now + 0.02); gain.gain.exponentialRampToValueAtTime(0.0001, now + (variant === 'soft' ? 0.12 : 0.18)); oscillator.connect(gain); gain.connect(context.destination); oscillator.start(now); oscillator.stop(now + (variant === 'soft' ? 0.12 : 0.18)); window.setTimeout(() => { void context.close().catch(() => undefined); }, variant === 'soft' ? 180 : 260); }