import AsyncStorage from '@react-native-async-storage/async-storage'; import * as Keychain from 'react-native-keychain'; import type { AuthSession } from '../../types/app'; const BIOMETRIC_KEY = 'bankcbs.biometricEnabled'; const SESSION_SERVICE = 'com.bankcbs.mobile.session'; /** The number and org last signed in with, so sign-in can prefill them. */ const PROFILE_SERVICE = 'com.bankcbs.mobile.lastProfile'; /** * The MPIN, held only when the customer opts into biometric sign-in. The API * has no refresh token — a session can only be opened by encrypting the MPIN — * so biometric sign-in is impossible without keeping it. It lives behind the * device's own biometric prompt and never in AsyncStorage. */ const BIOMETRIC_MPIN_SERVICE = 'com.bankcbs.mobile.biometricMpin'; export interface LastProfile { orgId: string; mobileNumber: string; fullName?: string; } /** * The request interceptor needs the token on every single call, and a Keystore * read costs tens of milliseconds on Android. Held in memory alongside the * Keychain copy, which stays the durable one across launches. */ let cachedSession: AuthSession | null = null; let sessionCacheLoaded = false; export const secureStorage = { async saveSession(session: AuthSession) { await Keychain.setGenericPassword('session', JSON.stringify(session), { service: SESSION_SERVICE, accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY, securityLevel: Keychain.SECURITY_LEVEL.SECURE_SOFTWARE, }); cachedSession = session; sessionCacheLoaded = true; }, async getSession(): Promise { if (sessionCacheLoaded) { return cachedSession; } const credentials = await Keychain.getGenericPassword({ service: SESSION_SERVICE, }); if (!credentials) { cachedSession = null; sessionCacheLoaded = true; return null; } try { cachedSession = JSON.parse(credentials.password) as AuthSession; sessionCacheLoaded = true; return cachedSession; } catch { // A session written by an older build of the app. Treat it as absent // rather than crashing the bootstrap. await this.clearSession(); return null; } }, async getAccessToken() { const session = await this.getSession(); return session?.sessionToken ?? null; }, async clearSession() { cachedSession = null; sessionCacheLoaded = true; await Keychain.resetGenericPassword({ service: SESSION_SERVICE, }); }, async saveLastProfile(profile: LastProfile) { await Keychain.setGenericPassword('lastProfile', JSON.stringify(profile), { service: PROFILE_SERVICE, accessible: Keychain.ACCESSIBLE.AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY, securityLevel: Keychain.SECURITY_LEVEL.SECURE_SOFTWARE, }); }, async getLastProfile(): Promise { const credentials = await Keychain.getGenericPassword({ service: PROFILE_SERVICE, }); if (!credentials) { return null; } try { return JSON.parse(credentials.password) as LastProfile; } catch { return null; } }, async clearLastProfile() { await Keychain.resetGenericPassword({ service: PROFILE_SERVICE }); }, /** * Stored under the platform's biometric access control, so reading it back * raises the fingerprint / Face ID prompt rather than the app faking one. */ async saveBiometricMpin(mpin: string) { await Keychain.setGenericPassword('mpin', mpin, { service: BIOMETRIC_MPIN_SERVICE, accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY, accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_CURRENT_SET_OR_DEVICE_PASSCODE, securityLevel: Keychain.SECURITY_LEVEL.SECURE_SOFTWARE, }); }, async getBiometricMpin(promptTitle: string): Promise { try { const credentials = await Keychain.getGenericPassword({ service: BIOMETRIC_MPIN_SERVICE, authenticationPrompt: { title: promptTitle }, }); return credentials ? credentials.password : null; } catch { // Cancelled prompt, or no enrolled biometric any more. return null; } }, async hasBiometricMpin() { try { const services = await Keychain.getAllGenericPasswordServices(); return services.includes(BIOMETRIC_MPIN_SERVICE); } catch { return false; } }, async clearBiometricMpin() { await Keychain.resetGenericPassword({ service: BIOMETRIC_MPIN_SERVICE }); }, async setBiometricEnabled(value: boolean) { await AsyncStorage.setItem(BIOMETRIC_KEY, JSON.stringify(value)); if (!value) { await this.clearBiometricMpin(); } }, async getBiometricEnabled() { const value = await AsyncStorage.getItem(BIOMETRIC_KEY); return value ? JSON.parse(value) : false; }, };