import { Capacitor } from '@capacitor/core' // Biometric login for the bundled native app (Phase 1). Stores a durable, REVOCABLE snapshot of the // session (refresh token + context — never the password) in the Android Keystore, released only on a // successful biometric match (getSecureCredentials). On unlock we mint a fresh access token via the // existing /api/app-auth/token-refresh, so no new server route is needed. // // Everything is native-only: the plugins are dynamically imported behind isNativePlatform(), so the // web build never loads @capgo/capacitor-native-biometric or @capacitor/preferences from here. const SERVER = 'app.logship.de' // namespace key for the stored credential const FLAG = 'logship_biometric_enabled' const native = () => Capacitor.isNativePlatform() const loadPlugins = async () => { const [bio, prefs] = await Promise.all([ import('@capgo/capacitor-native-biometric'), import('@capacitor/preferences'), ]) return { NativeBiometric: bio.NativeBiometric, AccessControl: bio.AccessControl, Preferences: prefs.Preferences } } export const useBiometricAuth = () => { // { available, type } — type is the BiometryType enum (3=fingerprint, 2/4=face, …). const isAvailable = async (): Promise<{ available: boolean, type: number }> => { if (!native()) return { available: false, type: 0 } try { const { NativeBiometric } = await loadPlugins() const r: any = await NativeBiometric.isAvailable({ useFallback: false }) return { available: !!r?.isAvailable, type: Number(r?.biometryType ?? 0) } } catch { return { available: false, type: 0 } } } const isEnabled = async (): Promise => { if (!native()) return false try { const { Preferences } = await loadPlugins() const { value } = await Preferences.get({ key: FLAG }) return value === 'true' } catch { return false } } // Store the snapshot biometric-bound. BIOMETRY_ANY keeps it usable when the user adds a new // fingerprint (convenience); switch to BIOMETRY_CURRENT_SET for stricter security (invalidates on // biometric enrollment changes). The stored secret is a revocable refresh token, not a password. const enable = async (snapshot: any, username?: string): Promise => { if (!native()) return false try { const { NativeBiometric, AccessControl, Preferences } = await loadPlugins() await NativeBiometric.deleteCredentials({ server: SERVER }).catch(() => {}) await NativeBiometric.setCredentials({ username: String(username || 'logship'), password: JSON.stringify(snapshot), server: SERVER, accessControl: AccessControl.BIOMETRY_ANY, }) await Preferences.set({ key: FLAG, value: 'true' }) return true } catch (e) { console.warn('[biometric] enable failed', e) return false } } // Prompts biometric and returns the stored snapshot, or null on cancel/lockout/biometrics-changed. const unlock = async (): Promise => { if (!native()) return null try { const { NativeBiometric } = await loadPlugins() const cred: any = await NativeBiometric.getSecureCredentials({ server: SERVER, title: 'LogShip', subtitle: 'Mit Biometrie anmelden', reason: 'Anmeldung bestätigen', negativeButtonText: 'Abbrechen', }) return cred?.password ? JSON.parse(cred.password) : null } catch (e) { console.warn('[biometric] unlock cancelled/failed', e) return null } } const disable = async (): Promise => { if (!native()) return try { const { NativeBiometric, Preferences } = await loadPlugins() await NativeBiometric.deleteCredentials({ server: SERVER }).catch(() => {}) await Preferences.remove({ key: FLAG }) } catch { /* non-fatal */ } } return { isAvailable, isEnabled, enable, unlock, disable } }