From 3cf732797cd3697aa0919627c700d263837a5c5d Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Wed, 15 Jul 2026 02:58:39 +0400 Subject: [PATCH] refactor: route localStorage access through shared LocalStorageService Sprint 2 high-priority cleanup: cart/language/location services called localStorage directly, bypassing the try/catch safety and core/ pattern used elsewhere (e.g. ProjectEditorDraftStorageService). New core/storage/LocalStorageService centralizes get/set/remove and JSON helpers with private-mode/quota error handling, reused across all three. Co-Authored-By: Claude Sonnet 5 --- src/app/core/storage/local-storage.service.ts | 53 +++++++++++++++++++ src/app/services/cart.service.ts | 11 ++-- src/app/services/language.service.ts | 11 ++-- src/app/services/location.service.ts | 19 +++---- 4 files changed, 73 insertions(+), 21 deletions(-) create mode 100644 src/app/core/storage/local-storage.service.ts diff --git a/src/app/core/storage/local-storage.service.ts b/src/app/core/storage/local-storage.service.ts new file mode 100644 index 0000000..cac9a8f --- /dev/null +++ b/src/app/core/storage/local-storage.service.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@angular/core'; + +/** + * Safe wrapper around window.localStorage — swallows quota/private-mode + * errors instead of throwing, so callers don't each need their own try/catch. + */ +@Injectable({ providedIn: 'root' }) +export class LocalStorageService { + getItem(key: string): string | null { + try { + return typeof localStorage !== 'undefined' ? localStorage.getItem(key) : null; + } catch { + return null; + } + } + + setItem(key: string, value: string): void { + try { + if (typeof localStorage !== 'undefined') { + localStorage.setItem(key, value); + } + } catch { + // storage unavailable (private mode / quota) - value simply won't persist + } + } + + removeItem(key: string): void { + try { + if (typeof localStorage !== 'undefined') { + localStorage.removeItem(key); + } + } catch { + // ignore + } + } + + getJSON(key: string): T | null { + const raw = this.getItem(key); + if (!raw) { + return null; + } + try { + return JSON.parse(raw) as T; + } catch { + this.removeItem(key); + return null; + } + } + + setJSON(key: string, value: T): void { + this.setItem(key, JSON.stringify(value)); + } +} diff --git a/src/app/services/cart.service.ts b/src/app/services/cart.service.ts index 559c67f..99ff51c 100644 --- a/src/app/services/cart.service.ts +++ b/src/app/services/cart.service.ts @@ -2,6 +2,7 @@ import { Injectable, signal, computed, effect, Injector } from '@angular/core'; import { DeliveryOption, CartItem } from '../models'; import { getDiscountedPrice } from '../utils/item.utils'; import { normalizeDeliveryOption, normalizeOptionalNumber } from '../utils/normalization.utils'; +import { LocalStorageService } from '../core/storage/local-storage.service'; import type { } from '../types/telegram.types'; type CartVariant = { colour?: string; size?: string; price?: number; currency?: string }; @@ -49,7 +50,7 @@ export class CartService { && items.some(item => (item.deliveryOptions?.length ?? 0) > 0 || item.selectedDelivery != null); }); - constructor(private injector: Injector) { + constructor(private injector: Injector, private readonly storage: LocalStorageService) { this.loadCart(); // Auto-save whenever cart changes (skip the initial empty state) @@ -131,10 +132,10 @@ export class CartService { private saveToStorage(items: CartItem[]): void { const data = JSON.stringify(items); - + // Always save to localStorage - localStorage.setItem(this.STORAGE_KEY, data); - + this.storage.setItem(this.STORAGE_KEY, data); + // Also save to Telegram CloudStorage if available if (this.isTelegram) { window.Telegram!.WebApp.CloudStorage.setItem(this.STORAGE_KEY, data, (err) => { @@ -167,7 +168,7 @@ export class CartService { } private loadFromLocalStorage(): void { - const stored = localStorage.getItem(this.STORAGE_KEY); + const stored = this.storage.getItem(this.STORAGE_KEY); if (stored) { this.parseAndSetCart(stored); } diff --git a/src/app/services/language.service.ts b/src/app/services/language.service.ts index d089ffb..d1b730c 100644 --- a/src/app/services/language.service.ts +++ b/src/app/services/language.service.ts @@ -1,5 +1,6 @@ import { Injectable, signal } from '@angular/core'; import { Router } from '@angular/router'; +import { LocalStorageService } from '../core/storage/local-storage.service'; export interface Language { code: string; @@ -38,14 +39,14 @@ export class LanguageService { currentLanguage = this.currentLanguageSignal.asReadonly(); currentCurrency = this.currentCurrencySignal.asReadonly(); - constructor(private router: Router) { + constructor(private router: Router, private readonly storage: LocalStorageService) { // Load saved language from localStorage - const savedLang = localStorage.getItem('selectedLanguage'); + const savedLang = this.storage.getItem('selectedLanguage'); if (savedLang && this.languages.find(l => l.code === savedLang && l.enabled)) { this.currentLanguageSignal.set(savedLang); } - const savedCurrency = localStorage.getItem('selectedCurrency'); + const savedCurrency = this.storage.getItem('selectedCurrency'); if (savedCurrency && this.currencies.find(c => c.code === savedCurrency)) { this.currentCurrencySignal.set(savedCurrency); } @@ -55,7 +56,7 @@ export class LanguageService { const lang = this.languages.find(l => l.code === langCode); if (lang && lang.enabled) { this.currentLanguageSignal.set(langCode); - localStorage.setItem('selectedLanguage', langCode); + this.storage.setItem('selectedLanguage', langCode); } } @@ -63,7 +64,7 @@ export class LanguageService { const currency = this.currencies.find(c => c.code === code); if (currency) { this.currentCurrencySignal.set(code); - localStorage.setItem('selectedCurrency', code); + this.storage.setItem('selectedCurrency', code); } } diff --git a/src/app/services/location.service.ts b/src/app/services/location.service.ts index aca3c3c..b272a56 100644 --- a/src/app/services/location.service.ts +++ b/src/app/services/location.service.ts @@ -2,6 +2,7 @@ import { Injectable, signal, computed } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Region, GeoIpResponse } from '../models/location.model'; import { ApiConfigService } from '../core/config/api-config.service'; +import { LocalStorageService } from '../core/storage/local-storage.service'; const STORAGE_KEY = 'selected_region'; @@ -28,7 +29,8 @@ export class LocationService { constructor( private readonly http: HttpClient, - private readonly apiConfig: ApiConfigService + private readonly apiConfig: ApiConfigService, + private readonly storage: LocalStorageService ) { this.loadRegions(); this.restoreFromStorage(); @@ -55,13 +57,13 @@ export class LocationService { /** Set region by user choice */ setRegion(region: Region): void { this.regionSignal.set(region); - localStorage.setItem(STORAGE_KEY, JSON.stringify(region)); + this.storage.setJSON(STORAGE_KEY, region); } /** Clear region (go global) */ clearRegion(): void { this.regionSignal.set(null); - localStorage.removeItem(STORAGE_KEY); + this.storage.removeItem(STORAGE_KEY); } /** Auto-detect user location via IP geolocation */ @@ -111,14 +113,9 @@ export class LocationService { /** Restore previously selected region from storage */ private restoreFromStorage(): void { - try { - const stored = localStorage.getItem(STORAGE_KEY); - if (stored) { - const region: Region = JSON.parse(stored); - this.regionSignal.set(region); - } - } catch { - localStorage.removeItem(STORAGE_KEY); + const region = this.storage.getJSON(STORAGE_KEY); + if (region) { + this.regionSignal.set(region); } }