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/<domain> 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 <noreply@anthropic.com>
This commit is contained in:
53
src/app/core/storage/local-storage.service.ts
Normal file
53
src/app/core/storage/local-storage.service.ts
Normal file
@@ -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<T>(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<T>(key: string, value: T): void {
|
||||||
|
this.setItem(key, JSON.stringify(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { Injectable, signal, computed, effect, Injector } from '@angular/core';
|
|||||||
import { DeliveryOption, CartItem } from '../models';
|
import { DeliveryOption, CartItem } from '../models';
|
||||||
import { getDiscountedPrice } from '../utils/item.utils';
|
import { getDiscountedPrice } from '../utils/item.utils';
|
||||||
import { normalizeDeliveryOption, normalizeOptionalNumber } from '../utils/normalization.utils';
|
import { normalizeDeliveryOption, normalizeOptionalNumber } from '../utils/normalization.utils';
|
||||||
|
import { LocalStorageService } from '../core/storage/local-storage.service';
|
||||||
import type { } from '../types/telegram.types';
|
import type { } from '../types/telegram.types';
|
||||||
|
|
||||||
type CartVariant = { colour?: string; size?: string; price?: number; currency?: string };
|
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);
|
&& 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();
|
this.loadCart();
|
||||||
|
|
||||||
// Auto-save whenever cart changes (skip the initial empty state)
|
// Auto-save whenever cart changes (skip the initial empty state)
|
||||||
@@ -131,10 +132,10 @@ export class CartService {
|
|||||||
|
|
||||||
private saveToStorage(items: CartItem[]): void {
|
private saveToStorage(items: CartItem[]): void {
|
||||||
const data = JSON.stringify(items);
|
const data = JSON.stringify(items);
|
||||||
|
|
||||||
// Always save to localStorage
|
// Always save to localStorage
|
||||||
localStorage.setItem(this.STORAGE_KEY, data);
|
this.storage.setItem(this.STORAGE_KEY, data);
|
||||||
|
|
||||||
// Also save to Telegram CloudStorage if available
|
// Also save to Telegram CloudStorage if available
|
||||||
if (this.isTelegram) {
|
if (this.isTelegram) {
|
||||||
window.Telegram!.WebApp.CloudStorage.setItem(this.STORAGE_KEY, data, (err) => {
|
window.Telegram!.WebApp.CloudStorage.setItem(this.STORAGE_KEY, data, (err) => {
|
||||||
@@ -167,7 +168,7 @@ export class CartService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private loadFromLocalStorage(): void {
|
private loadFromLocalStorage(): void {
|
||||||
const stored = localStorage.getItem(this.STORAGE_KEY);
|
const stored = this.storage.getItem(this.STORAGE_KEY);
|
||||||
if (stored) {
|
if (stored) {
|
||||||
this.parseAndSetCart(stored);
|
this.parseAndSetCart(stored);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Injectable, signal } from '@angular/core';
|
import { Injectable, signal } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
import { LocalStorageService } from '../core/storage/local-storage.service';
|
||||||
|
|
||||||
export interface Language {
|
export interface Language {
|
||||||
code: string;
|
code: string;
|
||||||
@@ -38,14 +39,14 @@ export class LanguageService {
|
|||||||
currentLanguage = this.currentLanguageSignal.asReadonly();
|
currentLanguage = this.currentLanguageSignal.asReadonly();
|
||||||
currentCurrency = this.currentCurrencySignal.asReadonly();
|
currentCurrency = this.currentCurrencySignal.asReadonly();
|
||||||
|
|
||||||
constructor(private router: Router) {
|
constructor(private router: Router, private readonly storage: LocalStorageService) {
|
||||||
// Load saved language from localStorage
|
// 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)) {
|
if (savedLang && this.languages.find(l => l.code === savedLang && l.enabled)) {
|
||||||
this.currentLanguageSignal.set(savedLang);
|
this.currentLanguageSignal.set(savedLang);
|
||||||
}
|
}
|
||||||
|
|
||||||
const savedCurrency = localStorage.getItem('selectedCurrency');
|
const savedCurrency = this.storage.getItem('selectedCurrency');
|
||||||
if (savedCurrency && this.currencies.find(c => c.code === savedCurrency)) {
|
if (savedCurrency && this.currencies.find(c => c.code === savedCurrency)) {
|
||||||
this.currentCurrencySignal.set(savedCurrency);
|
this.currentCurrencySignal.set(savedCurrency);
|
||||||
}
|
}
|
||||||
@@ -55,7 +56,7 @@ export class LanguageService {
|
|||||||
const lang = this.languages.find(l => l.code === langCode);
|
const lang = this.languages.find(l => l.code === langCode);
|
||||||
if (lang && lang.enabled) {
|
if (lang && lang.enabled) {
|
||||||
this.currentLanguageSignal.set(langCode);
|
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);
|
const currency = this.currencies.find(c => c.code === code);
|
||||||
if (currency) {
|
if (currency) {
|
||||||
this.currentCurrencySignal.set(code);
|
this.currentCurrencySignal.set(code);
|
||||||
localStorage.setItem('selectedCurrency', code);
|
this.storage.setItem('selectedCurrency', code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Injectable, signal, computed } from '@angular/core';
|
|||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Region, GeoIpResponse } from '../models/location.model';
|
import { Region, GeoIpResponse } from '../models/location.model';
|
||||||
import { ApiConfigService } from '../core/config/api-config.service';
|
import { ApiConfigService } from '../core/config/api-config.service';
|
||||||
|
import { LocalStorageService } from '../core/storage/local-storage.service';
|
||||||
|
|
||||||
const STORAGE_KEY = 'selected_region';
|
const STORAGE_KEY = 'selected_region';
|
||||||
|
|
||||||
@@ -28,7 +29,8 @@ export class LocationService {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly http: HttpClient,
|
private readonly http: HttpClient,
|
||||||
private readonly apiConfig: ApiConfigService
|
private readonly apiConfig: ApiConfigService,
|
||||||
|
private readonly storage: LocalStorageService
|
||||||
) {
|
) {
|
||||||
this.loadRegions();
|
this.loadRegions();
|
||||||
this.restoreFromStorage();
|
this.restoreFromStorage();
|
||||||
@@ -55,13 +57,13 @@ export class LocationService {
|
|||||||
/** Set region by user choice */
|
/** Set region by user choice */
|
||||||
setRegion(region: Region): void {
|
setRegion(region: Region): void {
|
||||||
this.regionSignal.set(region);
|
this.regionSignal.set(region);
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(region));
|
this.storage.setJSON(STORAGE_KEY, region);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Clear region (go global) */
|
/** Clear region (go global) */
|
||||||
clearRegion(): void {
|
clearRegion(): void {
|
||||||
this.regionSignal.set(null);
|
this.regionSignal.set(null);
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
this.storage.removeItem(STORAGE_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Auto-detect user location via IP geolocation */
|
/** Auto-detect user location via IP geolocation */
|
||||||
@@ -111,14 +113,9 @@ export class LocationService {
|
|||||||
|
|
||||||
/** Restore previously selected region from storage */
|
/** Restore previously selected region from storage */
|
||||||
private restoreFromStorage(): void {
|
private restoreFromStorage(): void {
|
||||||
try {
|
const region = this.storage.getJSON<Region>(STORAGE_KEY);
|
||||||
const stored = localStorage.getItem(STORAGE_KEY);
|
if (region) {
|
||||||
if (stored) {
|
this.regionSignal.set(region);
|
||||||
const region: Region = JSON.parse(stored);
|
|
||||||
this.regionSignal.set(region);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user