import type { ZodType } from 'zod'; import * as SecureStore from 'expo-secure-store'; function _isValidJson(value: string): boolean { try { JSON.parse(value); return true; } catch { return false; } } /** * A typed wrapper for expo-secure-store with optional Zod validation. */ export class TypedStore { /** A string that determines the namespace used for every key within the object. */ private namespace?: string; /** * @param namespace - Optional namespace for keys */ constructor(namespace?: string) { if (namespace) this.namespace = namespace; } /** * Returns a namespaced key. * @param key - The key to namespace. * @returns The namespaced key. */ private _getNamespacedKey(key: string): string { return this.namespace ? `${this.namespace}:${key}` : key; } /** * Helper function to serialize values for storage. */ private _serializeValue(value: T): string { const isPrimitive = typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'; return isPrimitive ? String(value) : JSON.stringify(value); } /** * Gets the current namespace. * @returns Can be undefined. */ getNamespace(): string | undefined { return this.namespace; } /** * Sets a new namespace. * @param namespace - The namespace. */ setNamespace(namespace: string): void { if (typeof namespace === 'string') { this.namespace = namespace; } } /** * Returns whether the SecureStore API is enabled on the current device. * This does not check the app permissions. * @returns True if available, false otherwise. */ static async isAvailableAsync(): Promise { return await SecureStore.isAvailableAsync(); } /** * Checks if the value can be saved with requireAuthentication option enabled. * @returns True if biometric authentication is available. * @platform android * @platform ios */ canUseBiometricAuthentication(): boolean { return SecureStore.canUseBiometricAuthentication(); } /** * Stores a value in the store. * @param key - The key to store the value under. * @param value - The value to store. */ setItem(key: string, value: T): void { const namespacedKey = this._getNamespacedKey(key); SecureStore.setItem(namespacedKey, this._serializeValue(value)); } /** * Stores a value in the store asynchronously. * @param key - The key to store the value under. * @param value - The value to store. */ async setItemAsync(key: string, value: T): Promise { const namespacedKey = this._getNamespacedKey(key); await SecureStore.setItemAsync(namespacedKey, this._serializeValue(value)); } /** * Stores a value in the store with an expiration time. * @param key - The key to store the value under. * @param value - The value to store. * @param ttl - Time to live in milliseconds. */ async setItemWithExpiration(key: string, value: T, ttl: number): Promise { const namespacedKey = this._getNamespacedKey(key); await SecureStore.setItemAsync(namespacedKey, this._serializeValue(value)); setTimeout(async () => await SecureStore.deleteItemAsync(namespacedKey), ttl); } /** * Retrieves a value from the store. * @param key - The key to retrieve the value from. * @returns The retrieved value or null if not found. */ getItem(key: string): T | null; /** * Retrieves a value from the store with a fallback. * @param key - The key to retrieve the value from. * @param fallback - The fallback value if key is not found. * @returns The retrieved value or fallback. */ getItem(key: string, fallback: T): T; /** * Retrieves a value from the store with a fallback and Zod validation. * @param key - The key to retrieve the value from. * @param fallback - The fallback value if key is not found or invalid. * @param schema - Zod schema to validate the parsed value. * @returns The retrieved value or fallback. */ getItem(key: string, fallback: T, schema: ZodType): T; getItem(key: string, fallback?: T, schema?: ZodType): T | null { const namespacedKey = this._getNamespacedKey(key); const item = SecureStore.getItem(namespacedKey); if (item === null) return fallback ?? null; const parsed = _isValidJson(item) ? JSON.parse(item) : item; if (schema) { const result = schema.safeParse(parsed); return result.success ? result.data : (fallback as T); } return parsed as T; } /** * Retrieves a value from the store asynchronously. * @param key - The key to retrieve the value from. * @returns The retrieved value or null if not found. */ getItemAsync(key: string): Promise; /** * Retrieves a value from the store asynchronously with a fallback. * @param key - The key to retrieve the value from. * @param fallback - The fallback value if key is not found. * @returns The retrieved value or fallback. */ getItemAsync(key: string, fallback: T): Promise; /** * Retrieves a value from the store asynchronously with a fallback and Zod validation. * @param key - The key to retrieve the value from. * @param fallback - The fallback value if key is not found or invalid. * @param schema - Zod schema to validate the parsed value. * @returns The retrieved value or fallback. */ getItemAsync(key: string, fallback: T, schema: ZodType): Promise; async getItemAsync(key: string, fallback?: T, schema?: ZodType): Promise { const namespacedKey = this._getNamespacedKey(key); const item = await SecureStore.getItemAsync(namespacedKey); if (item === null) return fallback ?? null; const parsed = _isValidJson(item) ? JSON.parse(item) : item; if (schema) { const result = schema.safeParse(parsed); return result.success ? result.data : (fallback as T); } return parsed as T; } /** * Removes a key from the store. * @param key - The key to remove. * @param options - Optional secure store options. */ async removeItemAsync(key: string, options?: SecureStore.SecureStoreOptions): Promise { const namespacedKey = this._getNamespacedKey(key); await SecureStore.deleteItemAsync(namespacedKey, options); } /** * Checks if a key exists in storage. * @param key - The key to check. * @returns True if exists, false otherwise. */ itemExists(key: string): boolean { const namespacedKey = this._getNamespacedKey(key); return SecureStore.getItem(namespacedKey) !== null; } /** * Checks if a key exists in storage asynchronously. * @param key - The key to check. * @returns True if exists, false otherwise. */ async itemExistsAsync(key: string): Promise { const namespacedKey = this._getNamespacedKey(key); const result = await SecureStore.getItemAsync(namespacedKey); return result !== null; } }