From 3588372cefc891ad07db820aa95de3ff5fd9589d Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Fri, 19 Sep 2025 11:33:31 -0500 Subject: [PATCH] Mobile Work --- frontend/src/core/store/navigation.ts | 10 +- frontend/src/core/units/UnitsContext.tsx | 23 ++-- frontend/src/core/utils/safe-storage.ts | 107 ++++++++++++++++++ .../fuel-logs/hooks/useFuelGrades.tsx | 12 ++ .../features/fuel-logs/hooks/useFuelLogs.tsx | 23 +++- .../settings/hooks/useSettingsPersistence.ts | 9 +- .../features/vehicles/hooks/useVehicles.ts | 18 +++ 7 files changed, 189 insertions(+), 13 deletions(-) create mode 100644 frontend/src/core/utils/safe-storage.ts diff --git a/frontend/src/core/store/navigation.ts b/frontend/src/core/store/navigation.ts index 69eec8b..e526834 100644 --- a/frontend/src/core/store/navigation.ts +++ b/frontend/src/core/store/navigation.ts @@ -1,5 +1,6 @@ import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; +import { safeStorage } from '../utils/safe-storage'; export type MobileScreen = 'Dashboard' | 'Vehicles' | 'Log Fuel' | 'Settings'; export type VehicleSubScreen = 'list' | 'detail' | 'add' | 'edit'; @@ -193,13 +194,20 @@ export const useNavigationStore = create()( }), { name: 'motovaultpro-mobile-navigation', - storage: createJSONStorage(() => localStorage), + storage: createJSONStorage(() => safeStorage), partialize: (state) => ({ activeScreen: state.activeScreen, vehicleSubScreen: state.vehicleSubScreen, selectedVehicleId: state.selectedVehicleId, formStates: state.formStates, }), + onRehydrateStorage: () => (state) => { + if (state) { + console.log('[Navigation] State rehydrated successfully'); + } else { + console.warn('[Navigation] State rehydration failed, using defaults'); + } + }, } ) ); \ No newline at end of file diff --git a/frontend/src/core/units/UnitsContext.tsx b/frontend/src/core/units/UnitsContext.tsx index 2816833..56e7637 100644 --- a/frontend/src/core/units/UnitsContext.tsx +++ b/frontend/src/core/units/UnitsContext.tsx @@ -5,6 +5,7 @@ import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'; import { UnitSystem, UnitPreferences } from './units.types'; +import { safeStorage } from '../utils/safe-storage'; import { formatDistanceBySystem, formatVolumeBySystem, @@ -48,18 +49,26 @@ export const UnitsProvider: React.FC = ({ }) => { const [unitSystem, setUnitSystem] = useState(initialSystem); - // Load unit preference from localStorage on mount + // Load unit preference from storage on mount useEffect(() => { - const stored = localStorage.getItem('motovaultpro-unit-system'); - if (stored === 'imperial' || stored === 'metric') { - setUnitSystem(stored); + try { + const stored = safeStorage.getItem('motovaultpro-unit-system'); + if (stored === 'imperial' || stored === 'metric') { + setUnitSystem(stored); + } + } catch (error) { + console.warn('[Units] Failed to load unit system preference:', error); } }, []); - - // Save unit preference to localStorage when changed + + // Save unit preference to storage when changed const handleSetUnitSystem = (system: UnitSystem) => { setUnitSystem(system); - localStorage.setItem('motovaultpro-unit-system', system); + try { + safeStorage.setItem('motovaultpro-unit-system', system); + } catch (error) { + console.warn('[Units] Failed to save unit system preference:', error); + } }; // Generate preferences object based on current system diff --git a/frontend/src/core/utils/safe-storage.ts b/frontend/src/core/utils/safe-storage.ts new file mode 100644 index 0000000..3d1fc7d --- /dev/null +++ b/frontend/src/core/utils/safe-storage.ts @@ -0,0 +1,107 @@ +/** + * @ai-summary Safe localStorage wrapper for mobile browsers + * @ai-context Prevents errors when localStorage is blocked in mobile browsers + */ + +// Safe localStorage wrapper that won't crash on mobile browsers +const createSafeStorage = () => { + let isAvailable = false; + + // Test localStorage availability + try { + const testKey = '__motovaultpro_storage_test__'; + localStorage.setItem(testKey, 'test'); + localStorage.removeItem(testKey); + isAvailable = true; + } catch (error) { + console.warn('[Storage] localStorage not available, using memory fallback:', error); + isAvailable = false; + } + + // Memory fallback when localStorage is blocked + const memoryStorage = new Map(); + + return { + getItem: (key: string): string | null => { + try { + if (isAvailable) { + return localStorage.getItem(key); + } else { + return memoryStorage.get(key) || null; + } + } catch (error) { + console.warn('[Storage] getItem failed, using memory fallback:', error); + return memoryStorage.get(key) || null; + } + }, + + setItem: (key: string, value: string): void => { + try { + if (isAvailable) { + localStorage.setItem(key, value); + } else { + memoryStorage.set(key, value); + } + } catch (error) { + console.warn('[Storage] setItem failed, using memory fallback:', error); + memoryStorage.set(key, value); + } + }, + + removeItem: (key: string): void => { + try { + if (isAvailable) { + localStorage.removeItem(key); + } else { + memoryStorage.delete(key); + } + } catch (error) { + console.warn('[Storage] removeItem failed, using memory fallback:', error); + memoryStorage.delete(key); + } + }, + + // For zustand createJSONStorage compatibility + key: (index: number): string | null => { + try { + if (isAvailable) { + return localStorage.key(index); + } else { + const keys = Array.from(memoryStorage.keys()); + return keys[index] || null; + } + } catch (error) { + console.warn('[Storage] key access failed:', error); + return null; + } + }, + + get length(): number { + try { + if (isAvailable) { + return localStorage.length; + } else { + return memoryStorage.size; + } + } catch (error) { + console.warn('[Storage] length access failed:', error); + return 0; + } + }, + + clear: (): void => { + try { + if (isAvailable) { + localStorage.clear(); + } else { + memoryStorage.clear(); + } + } catch (error) { + console.warn('[Storage] clear failed:', error); + memoryStorage.clear(); + } + } + }; +}; + +export const safeStorage = createSafeStorage(); \ No newline at end of file diff --git a/frontend/src/features/fuel-logs/hooks/useFuelGrades.tsx b/frontend/src/features/fuel-logs/hooks/useFuelGrades.tsx index 9646629..2319184 100644 --- a/frontend/src/features/fuel-logs/hooks/useFuelGrades.tsx +++ b/frontend/src/features/fuel-logs/hooks/useFuelGrades.tsx @@ -1,11 +1,23 @@ import { useQuery } from '@tanstack/react-query'; +import { useAuth0 } from '@auth0/auth0-react'; import { fuelLogsApi } from '../api/fuel-logs.api'; import { FuelType, FuelGradeOption } from '../types/fuel-logs.types'; export const useFuelGrades = (fuelType: FuelType) => { + const { isAuthenticated, isLoading: authLoading } = useAuth0(); const { data, isLoading, error } = useQuery({ queryKey: ['fuelGrades', fuelType], queryFn: () => fuelLogsApi.getFuelGrades(fuelType), + enabled: isAuthenticated && !authLoading, + retry: (failureCount, error: any) => { + // Retry 401 errors up to 3 times for mobile auth timing issues + if (error?.response?.status === 401 && failureCount < 3) { + console.log(`[Mobile Auth] Fuel grades API retry ${failureCount + 1}/3 for 401 error`); + return true; + } + return false; + }, + retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), }); return { fuelGrades: data || [], isLoading, error }; }; diff --git a/frontend/src/features/fuel-logs/hooks/useFuelLogs.tsx b/frontend/src/features/fuel-logs/hooks/useFuelLogs.tsx index 7c4a839..63e758d 100644 --- a/frontend/src/features/fuel-logs/hooks/useFuelLogs.tsx +++ b/frontend/src/features/fuel-logs/hooks/useFuelLogs.tsx @@ -1,19 +1,40 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useAuth0 } from '@auth0/auth0-react'; import { fuelLogsApi } from '../api/fuel-logs.api'; import { CreateFuelLogRequest, FuelLogResponse, EnhancedFuelStats } from '../types/fuel-logs.types'; export const useFuelLogs = (vehicleId?: string) => { + const { isAuthenticated, isLoading } = useAuth0(); const queryClient = useQueryClient(); const logsQuery = useQuery({ queryKey: ['fuelLogs', vehicleId || 'all'], queryFn: () => (vehicleId ? fuelLogsApi.getFuelLogsByVehicle(vehicleId) : fuelLogsApi.getUserFuelLogs()), + enabled: isAuthenticated && !isLoading, + retry: (failureCount, error: any) => { + // Retry 401 errors up to 3 times for mobile auth timing issues + if (error?.response?.status === 401 && failureCount < 3) { + console.log(`[Mobile Auth] Fuel logs API retry ${failureCount + 1}/3 for 401 error`); + return true; + } + return false; + }, + retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), }); const statsQuery = useQuery({ queryKey: ['fuelLogsStats', vehicleId], queryFn: () => fuelLogsApi.getVehicleStats(vehicleId!), - enabled: !!vehicleId, + enabled: !!vehicleId && isAuthenticated && !isLoading, + retry: (failureCount, error: any) => { + // Retry 401 errors up to 3 times for mobile auth timing issues + if (error?.response?.status === 401 && failureCount < 3) { + console.log(`[Mobile Auth] Fuel stats API retry ${failureCount + 1}/3 for 401 error`); + return true; + } + return false; + }, + retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), }); const createMutation = useMutation({ diff --git a/frontend/src/features/settings/hooks/useSettingsPersistence.ts b/frontend/src/features/settings/hooks/useSettingsPersistence.ts index 3fd1fa0..cdc5ae0 100644 --- a/frontend/src/features/settings/hooks/useSettingsPersistence.ts +++ b/frontend/src/features/settings/hooks/useSettingsPersistence.ts @@ -1,4 +1,5 @@ import { useCallback } from 'react'; +import { safeStorage } from '../../../core/utils/safe-storage'; export interface SettingsState { darkMode: boolean; @@ -15,19 +16,19 @@ const SETTINGS_STORAGE_KEY = 'motovaultpro-mobile-settings'; export const useSettingsPersistence = () => { const loadSettings = useCallback((): SettingsState | null => { try { - const stored = localStorage.getItem(SETTINGS_STORAGE_KEY); + const stored = safeStorage.getItem(SETTINGS_STORAGE_KEY); return stored ? JSON.parse(stored) : null; } catch (error) { - console.error('Error loading settings:', error); + console.error('[Settings] Error loading settings:', error); return null; } }, []); const saveSettings = useCallback((settings: SettingsState) => { try { - localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(settings)); + safeStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(settings)); } catch (error) { - console.error('Error saving settings:', error); + console.error('[Settings] Error saving settings:', error); } }, []); diff --git a/frontend/src/features/vehicles/hooks/useVehicles.ts b/frontend/src/features/vehicles/hooks/useVehicles.ts index 38064dd..32cdd2c 100644 --- a/frontend/src/features/vehicles/hooks/useVehicles.ts +++ b/frontend/src/features/vehicles/hooks/useVehicles.ts @@ -23,6 +23,15 @@ export const useVehicles = () => { queryKey: ['vehicles'], queryFn: vehiclesApi.getAll, enabled: isAuthenticated && !isLoading, + retry: (failureCount, error: any) => { + // Retry 401 errors up to 3 times for mobile auth timing issues + if (error?.response?.status === 401 && failureCount < 3) { + console.log(`[Mobile Auth] API retry ${failureCount + 1}/3 for 401 error`); + return true; + } + return false; + }, + retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), }); }; @@ -32,6 +41,15 @@ export const useVehicle = (id: string) => { queryKey: ['vehicles', id], queryFn: () => vehiclesApi.getById(id), enabled: !!id && isAuthenticated && !isLoading, + retry: (failureCount, error: any) => { + // Retry 401 errors up to 3 times for mobile auth timing issues + if (error?.response?.status === 401 && failureCount < 3) { + console.log(`[Mobile Auth] API retry ${failureCount + 1}/3 for 401 error`); + return true; + } + return false; + }, + retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), }); };