/** * AsyncStorage Token Storage for React Native Mobile Platforms * * Bundler-agnostic AsyncStorage implementation for mobile platforms. * * IMPORTANT: Due to bundler conflicts between Rollup (SDK) and Metro (app), * the consuming app MUST set global.AsyncStorageModule before using this SDK: * * ```javascript * // In your app's index.js, BEFORE importing the SDK: * import AsyncStorage from '@react-native-async-storage/async-storage'; * global.AsyncStorageModule = AsyncStorage; * ``` */ import type { TokenStorage } from '@explorins/pers-sdk/core'; // Declare global type for TypeScript declare global { var AsyncStorageModule: any; } /** * Get AsyncStorage from the global that the app must set. * * This bypasses all bundler resolution issues by having the app * explicitly provide the AsyncStorage module via a global variable. */ function getAsyncStorage(): any { // Check if the app has set the global if (global.AsyncStorageModule) { const module = global.AsyncStorageModule; // Validate it has the expected methods if (typeof module?.getItem === 'function') { console.log('[AsyncStorage] Using global.AsyncStorageModule (direct)'); return module; } else if (typeof module?.default?.getItem === 'function') { console.log('[AsyncStorage] Using global.AsyncStorageModule.default'); return module.default; } } // Fallback: try require (may not work due to bundler conflicts) try { // eslint-disable-next-line @typescript-eslint/no-var-requires const module = require('@react-native-async-storage/async-storage'); if (typeof module?.default?.getItem === 'function') { console.log('[AsyncStorage] Fallback: using require().default'); return module.default; } else if (typeof module?.getItem === 'function') { console.log('[AsyncStorage] Fallback: using require() direct'); return module; } } catch (e) { // Ignore require errors } // If we get here, provide a helpful error throw new Error( 'AsyncStorage not available. Please add this to your app\'s index.js BEFORE importing the SDK:\n\n' + ' import AsyncStorage from \'@react-native-async-storage/async-storage\';\n' + ' global.AsyncStorageModule = AsyncStorage;\n\n' + 'This is required due to bundler conflicts between Rollup and Metro.' ); } /** * Bundler-agnostic AsyncStorage wrapper * Uses global injection to avoid bundler conflicts */ class BundlerAgnosticAsyncStorage { private storage: any; constructor() { // Get AsyncStorage at runtime, not from a potentially corrupted static import this.storage = getAsyncStorage(); // Validate that we have the required methods const requiredMethods = ['getItem', 'setItem', 'removeItem', 'getAllKeys', 'multiRemove']; const missingMethods = requiredMethods.filter(method => typeof this.storage[method] !== 'function'); if (missingMethods.length > 0) { throw new Error( `AsyncStorage missing required methods: ${missingMethods.join(', ')}. ` + 'Ensure @react-native-async-storage/async-storage is properly installed and compatible.' ); } } async getItem(key: string): Promise { return this.storage.getItem(key); } async setItem(key: string, value: string): Promise { return this.storage.setItem(key, value); } async removeItem(key: string): Promise { return this.storage.removeItem(key); } async getAllKeys(): Promise { return this.storage.getAllKeys(); } async multiRemove(keys: string[]): Promise { return this.storage.multiRemove(keys); } } /** * AsyncStorage implementation for mobile platforms * * This class is only used on mobile platforms (iOS/Android). * Web platforms use LocalStorageTokenStorage from core SDK. */ export class AsyncStorageTokenStorage implements TokenStorage { private keyPrefix: string; private asyncStorage: BundlerAgnosticAsyncStorage; constructor(keyPrefix: string = 'pers_tokens_') { this.keyPrefix = keyPrefix; try { // Initialize bundler-agnostic AsyncStorage wrapper // No longer passes the module - it uses runtime require() internally this.asyncStorage = new BundlerAgnosticAsyncStorage(); } catch (error) { console.error('[AsyncStorageTokenStorage] Failed to initialize:', error); throw new Error( 'Failed to initialize AsyncStorage. Make sure @react-native-async-storage/async-storage is installed and ' + 'this code is running on a React Native mobile platform (not web).' ); } } async set(key: string, value: string): Promise { try { const fullKey = `${this.keyPrefix}${key}`; await this.asyncStorage.setItem(fullKey, value); console.log(`[AsyncStorage] Token stored: ${key}`); } catch (error) { console.error(`[AsyncStorage] Failed to store token ${key}:`, error); throw new Error(`Token storage failed: ${error}`); } } async get(key: string): Promise { try { const fullKey = `${this.keyPrefix}${key}`; const value = await this.asyncStorage.getItem(fullKey); console.log(`[AsyncStorage] Token retrieved: ${key} - ${value ? 'exists' : 'null'}`); return value; } catch (error) { console.error(`[AsyncStorage] Failed to retrieve token ${key}:`, error); return null; } } async remove(key: string): Promise { try { await this.asyncStorage.removeItem(`${this.keyPrefix}${key}`); } catch (error) { console.error(`Failed to remove token ${key}:`, error); } } async clear(): Promise { try { const allKeys = await this.asyncStorage.getAllKeys(); const ourKeys = allKeys.filter(key => key.startsWith(this.keyPrefix)); if (ourKeys.length > 0) { console.log(`[AsyncStorage] Clearing ${ourKeys.length} token(s):`, ourKeys); await this.asyncStorage.multiRemove(ourKeys); console.log('[AsyncStorage] All tokens cleared successfully'); } else { console.log('[AsyncStorage] No tokens to clear'); } } catch (error) { console.error('[AsyncStorage] Failed to clear token storage:', error); } } }