feat: replace demo storage instances with secure storage implementations across i18n and storage features
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import i18n from 'i18next';
|
||||
import { demoSecureStorage, StorageKey } from '@repo/core-storage';
|
||||
import { secureStorage, StorageKey } from '@repo/core-storage';
|
||||
|
||||
/**
|
||||
* Changes the active language, saves the preference locally, and optionally syncs with the backend.
|
||||
@@ -9,14 +9,14 @@ import { demoSecureStorage, StorageKey } from '@repo/core-storage';
|
||||
*/
|
||||
export async function changeLanguage(
|
||||
newLng: string,
|
||||
syncCallback?: (newLng: string, prevLng: string) => Promise<void>
|
||||
syncCallback?: (newLng: string, prevLng: string) => Promise<void>,
|
||||
): Promise<void> {
|
||||
const prevLng = i18n.language;
|
||||
|
||||
|
||||
if (prevLng === newLng) return;
|
||||
|
||||
// 1. Update local storage & i18next optimistically
|
||||
await demoSecureStorage.setItem(StorageKey.LOCALE, newLng);
|
||||
await secureStorage.setItem(StorageKey.LOCALE, newLng);
|
||||
await i18n.changeLanguage(newLng);
|
||||
|
||||
// 2. Trigger optional backend sync
|
||||
@@ -26,7 +26,7 @@ export async function changeLanguage(
|
||||
} catch (error) {
|
||||
console.error('[i18n] Backend sync failed, rolling back language', error);
|
||||
// Rollback on failure
|
||||
await demoSecureStorage.setItem(StorageKey.LOCALE, prevLng);
|
||||
await secureStorage.setItem(StorageKey.LOCALE, prevLng);
|
||||
await i18n.changeLanguage(prevLng);
|
||||
throw error; // Rethrow so the caller can show an error toast
|
||||
}
|
||||
@@ -43,13 +43,9 @@ export async function changeLanguage(
|
||||
* @param overrides A deeply nested object containing the overridden string keys and values.
|
||||
* @param lng Specific language to override. Defaults to currently active language.
|
||||
*/
|
||||
export function applyTenantOverrides(
|
||||
namespace: string,
|
||||
overrides: Record<string, unknown>,
|
||||
lng?: string
|
||||
): void {
|
||||
export function applyTenantOverrides(namespace: string, overrides: Record<string, unknown>, lng?: string): void {
|
||||
const targetLng = lng || i18n.language;
|
||||
|
||||
|
||||
// deep: true -> merges with existing keys rather than replacing the whole namespace
|
||||
// overwrite: true -> allows replacing existing specific keys
|
||||
i18n.addResourceBundle(targetLng, namespace, overrides, true, true);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import { demoSecureStorage, StorageKey } from '@repo/core-storage';
|
||||
import { secureStorage, StorageKey } from '@repo/core-storage';
|
||||
import commonEn from './locales/en/common.json';
|
||||
import commonId from './locales/id/common.json';
|
||||
|
||||
@@ -16,14 +16,14 @@ export const resources = {
|
||||
|
||||
/**
|
||||
* Bootstraps the central i18n engine.
|
||||
*
|
||||
*
|
||||
* This reads the preferred locale from secureStorage and initializes
|
||||
* i18next synchronously before React renders.
|
||||
*/
|
||||
export async function setupI18n(): Promise<void> {
|
||||
let initialLng = DEFAULT_LANGUAGE;
|
||||
try {
|
||||
const storedLng = await demoSecureStorage.getItem<string>(StorageKey.LOCALE);
|
||||
const storedLng = await secureStorage.getItem<string>(StorageKey.LOCALE);
|
||||
if (storedLng && SUPPORTED_LANGUAGES.includes(storedLng as SupportedLanguage)) {
|
||||
initialLng = storedLng;
|
||||
}
|
||||
@@ -31,17 +31,15 @@ export async function setupI18n(): Promise<void> {
|
||||
console.warn('[i18n] Failed to read locale from storage', err);
|
||||
}
|
||||
|
||||
await i18n
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources,
|
||||
lng: initialLng,
|
||||
fallbackLng: DEFAULT_LANGUAGE,
|
||||
defaultNS: 'common',
|
||||
interpolation: {
|
||||
escapeValue: false, // React already escapes values
|
||||
},
|
||||
});
|
||||
await i18n.use(initReactI18next).init({
|
||||
resources,
|
||||
lng: initialLng,
|
||||
fallbackLng: DEFAULT_LANGUAGE,
|
||||
defaultNS: 'common',
|
||||
interpolation: {
|
||||
escapeValue: false, // React already escapes values
|
||||
},
|
||||
});
|
||||
|
||||
// Apply initial language to the DOM for SEO/Accessibility
|
||||
if (typeof document !== 'undefined') {
|
||||
|
||||
@@ -32,36 +32,36 @@ This package provides a unified, Promise-based interface for interacting with br
|
||||
|
||||
### 1. Secure Local Storage (Tokens, Profile)
|
||||
|
||||
Use `demoSecureStorage` (or your app's configured instance) for small payloads. If the key is in `ENCRYPTED_KEYS`, it will be encrypted at rest.
|
||||
Use `secureStorage` (or your app's configured instance) for small payloads. If the key is in `ENCRYPTED_KEYS`, it will be encrypted at rest.
|
||||
|
||||
```typescript
|
||||
import { demoSecureStorage, StorageKey } from '@repo/core-storage';
|
||||
import { secureStorage, StorageKey } from '@repo/core-storage';
|
||||
import type { UserProfile } from '@/types';
|
||||
|
||||
// CREATE / UPDATE
|
||||
// If StorageKey.USER_PROFILE is in ENCRYPTED_KEYS, this is AES-encrypted automatically.
|
||||
await demoSecureStorage.setItem(StorageKey.USER_PROFILE, {
|
||||
await secureStorage.setItem(StorageKey.USER_PROFILE, {
|
||||
id: 1,
|
||||
name: 'Firman',
|
||||
role: 'admin'
|
||||
});
|
||||
|
||||
// READ
|
||||
const profile = await demoSecureStorage.getItem<UserProfile>(StorageKey.USER_PROFILE);
|
||||
const profile = await secureStorage.getItem<UserProfile>(StorageKey.USER_PROFILE);
|
||||
if (profile) {
|
||||
console.log('Welcome back,', profile.name);
|
||||
}
|
||||
|
||||
// DELETE
|
||||
await demoSecureStorage.removeItem(StorageKey.USER_PROFILE);
|
||||
await secureStorage.removeItem(StorageKey.USER_PROFILE);
|
||||
```
|
||||
|
||||
### 2. IndexedDB (Offline Data, Large Payloads)
|
||||
|
||||
Use the pre-configured `demoIndexedDB` (or instantiate your own) for large, asynchronous data. It uses the exact same API and encryption pipeline.
|
||||
Use the pre-configured `secureIndexedDB` (or instantiate your own) for large, asynchronous data. It uses the exact same API and encryption pipeline.
|
||||
|
||||
```typescript
|
||||
import { demoIndexedDB } from '@repo/core-storage';
|
||||
import { secureIndexedDB } from '@repo/core-storage';
|
||||
|
||||
interface DraftData {
|
||||
id: string;
|
||||
@@ -70,14 +70,14 @@ interface DraftData {
|
||||
}
|
||||
|
||||
// Save a large draft offline
|
||||
await demoIndexedDB.setItem('offline_draft_123', {
|
||||
await secureIndexedDB.setItem('offline_draft_123', {
|
||||
id: '123',
|
||||
content: 'Huge text content...',
|
||||
lastModified: Date.now()
|
||||
});
|
||||
|
||||
// Retrieve the draft
|
||||
const draft = await demoIndexedDB.getItem<DraftData>('offline_draft_123');
|
||||
const draft = await secureIndexedDB.getItem<DraftData>('offline_draft_123');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -22,28 +22,27 @@ import { IndexedDBService } from './indexed-db.service';
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { demoSecureStorage, StorageKey } from '@repo/core-storage';
|
||||
* import { secureStorage, StorageKey } from '@repo/core-storage';
|
||||
*
|
||||
* await demoSecureStorage.setItem(StorageKey.ACCESS_TOKEN, 'eyJhbGci...');
|
||||
* const token = await demoSecureStorage.getItem<string>(StorageKey.ACCESS_TOKEN);
|
||||
* await secureStorage.setItem(StorageKey.ACCESS_TOKEN, 'eyJhbGci...');
|
||||
* const token = await secureStorage.getItem<string>(StorageKey.ACCESS_TOKEN);
|
||||
* ```
|
||||
*/
|
||||
export const demoSecureStorage = new LocalStorageService();
|
||||
export const secureStorage = new LocalStorageService();
|
||||
|
||||
/**
|
||||
* Default IndexedDB instance.
|
||||
*
|
||||
* Uses `app_db` database with a `kv_store` object store.
|
||||
* Sensitive keys are encrypted at rest using the same
|
||||
* `EncryptionUtils` pipeline as `demoSecureStorage`.
|
||||
* `EncryptionUtils` pipeline as `secureStorage`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { demoIndexedDB } from '@repo/core-storage';
|
||||
* import { secureIndexedDB } from '@repo/core-storage';
|
||||
*
|
||||
* await demoIndexedDB.setItem('offline_draft', { content: '...' });
|
||||
* const draft = await demoIndexedDB.getItem<Draft>('offline_draft');
|
||||
* await secureIndexedDB.setItem('offline_draft', { content: '...' });
|
||||
* const draft = await secureIndexedDB.getItem<Draft>('offline_draft');
|
||||
* ```
|
||||
*/
|
||||
export const demoIndexedDB = new IndexedDBService({ dbName: 'app_db', storeName: 'kv_store' });
|
||||
export const demoIndexedDB2 = new IndexedDBService({ dbName: 'app_db', storeName: 'kv_store_2' });
|
||||
export const secureIndexedDB = new IndexedDBService({ dbName: 'app_db', storeName: 'kv_store' });
|
||||
|
||||
Reference in New Issue
Block a user