Files
pwap/mobile/src/contexts/BiometricAuthContext.tsx
T
Claude c01abc79df test(mobile): add comprehensive test suite - 38% coverage achieved
Added complete testing infrastructure with 160 passing tests across 34 suites:

 Test Infrastructure Setup:
- Created babel.config.cjs with Expo preset
- Configured jest.config.cjs with proper transformIgnorePatterns
- Added jest.setup.cjs with comprehensive mocks
- Added jest.setup.before.cjs for pre-setup configuration
- Created __mocks__/ directory for custom mocks

 Component Tests (10 test files):
- Badge.test.tsx (13 tests) - 100% coverage
- Button.test.tsx (14 tests) - 100% statements
- Card.test.tsx (7 tests)
- Input.test.tsx (10 tests)
- LoadingSkeleton.test.tsx (10 tests) - 93% coverage
- TokenIcon.test.tsx (7 tests) - 100% coverage
- BottomSheet.test.tsx (9 tests)
- index.test.ts (1 test)

 Context Tests (4 test files):
- AuthContext.test.tsx (7 tests)
- PolkadotContext.test.tsx (10 tests)
- BiometricAuthContext.test.tsx (11 tests)
- LanguageContext.test.tsx (9 tests)

 Screen Tests (16 test files):
- All major screens tested with provider wrappers
- WelcomeScreen, SignIn/SignUp, Dashboard
- Wallet, Swap, Staking, Governance
- P2P, NFT Gallery, Education, Forum
- BeCitizen, Security, Lock, Referral, Profile

 Utility Tests:
- i18n/index.test.ts (4 tests)
- lib/supabase.test.ts (3 tests)
- theme/colors.test.ts (2 tests)

 App Integration Test:
- App.test.tsx (3 tests)

Coverage Metrics:
- Statements: 37.74% (target: 35%)
- Branches: 23.94% (target: 20%)
- Functions: 28.53% (target: 25%)
- Lines: 39.73% (target: 35%)

All coverage thresholds met! 

Test Results:
- 34/34 test suites passing
- 160/160 tests passing
- 17 snapshots

Key Improvements:
- Fixed ProfileScreen.tsx import bug (react-native import)
- Added comprehensive mocks for Polkadot, Expo, Supabase
- Created test-utils.tsx for provider wrappers
- All tests use proper async/await patterns
- Proper cleanup with React Testing Library

Production Ready: Test infrastructure is complete and extensible.
2025-11-23 06:34:58 +00:00

349 lines
10 KiB
TypeScript

import React, { createContext, useContext, useState, useEffect } from 'react';
import * as LocalAuthentication from 'expo-local-authentication';
import * as SecureStore from 'expo-secure-store';
import AsyncStorage from '@react-native-async-storage/async-storage';
/**
* CRITICAL SECURITY NOTE:
* ALL DATA STAYS ON DEVICE - NEVER SENT TO SERVER
*
* Storage Strategy:
* - Biometric settings: AsyncStorage (local device only)
* - PIN code: SecureStore (encrypted on device)
* - Lock timer: AsyncStorage (local device only)
* - Last unlock time: AsyncStorage (local device only)
*
* NO DATA IS EVER TRANSMITTED TO EXTERNAL SERVERS
*/
const BIOMETRIC_ENABLED_KEY = '@biometric_enabled'; // Local only
const PIN_CODE_KEY = 'user_pin_code'; // Encrypted SecureStore
const AUTO_LOCK_TIMER_KEY = '@auto_lock_timer'; // Local only
const LAST_UNLOCK_TIME_KEY = '@last_unlock_time'; // Local only
interface BiometricAuthContextType {
isBiometricSupported: boolean;
isBiometricEnrolled: boolean;
isBiometricAvailable: boolean;
biometricType: 'fingerprint' | 'facial' | 'iris' | 'none';
isBiometricEnabled: boolean;
isLocked: boolean;
autoLockTimer: number; // minutes
authenticate: () => Promise<boolean>;
enableBiometric: () => Promise<boolean>;
disableBiometric: () => Promise<void>;
setPinCode: (pin: string) => Promise<void>;
verifyPinCode: (pin: string) => Promise<boolean>;
setAutoLockTimer: (minutes: number) => Promise<void>;
lock: () => void;
unlock: () => void;
checkAutoLock: () => Promise<void>;
}
const BiometricAuthContext = createContext<BiometricAuthContextType | undefined>(
undefined
);
export const BiometricAuthProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [isBiometricSupported, setIsBiometricSupported] = useState(false);
const [isBiometricEnrolled, setIsBiometricEnrolled] = useState(false);
const [biometricType, setBiometricType] = useState<'fingerprint' | 'facial' | 'iris' | 'none'>('none');
const [isBiometricEnabled, setIsBiometricEnabled] = useState(false);
const [isLocked, setIsLocked] = useState(true);
const [autoLockTimer, setAutoLockTimerState] = useState(5); // Default 5 minutes
// Computed: biometrics are available if hardware supports AND user has enrolled
const isBiometricAvailable = isBiometricSupported && isBiometricEnrolled;
/**
* Check if app should auto-lock
* All checks happen LOCALLY
*/
const checkAutoLock = React.useCallback(async (): Promise<void> => {
try {
// Get last unlock time from LOCAL storage
const lastUnlockTime = await AsyncStorage.getItem(LAST_UNLOCK_TIME_KEY);
if (!lastUnlockTime) {
// First time or no previous unlock - lock the app
setIsLocked(true);
return;
}
const lastUnlock = parseInt(lastUnlockTime, 10);
const now = Date.now();
const minutesPassed = (now - lastUnlock) / 1000 / 60;
// If more time passed than timer, lock the app
if (minutesPassed >= autoLockTimer) {
setIsLocked(true);
} else {
setIsLocked(false);
}
} catch (error) {
if (__DEV__) console.error('Check auto-lock error:', error);
// On error, lock for safety
setIsLocked(true);
}
}, [autoLockTimer]);
/**
* Initialize biometric capabilities
* Checks device support - NO DATA SENT ANYWHERE
*/
const initBiometric = React.useCallback(async () => {
try {
// Check if device supports biometrics
const compatible = await LocalAuthentication.hasHardwareAsync();
setIsBiometricSupported(compatible);
if (compatible) {
// Check if user has enrolled biometrics
const enrolled = await LocalAuthentication.isEnrolledAsync();
setIsBiometricEnrolled(enrolled);
// Get supported authentication types
const types = await LocalAuthentication.supportedAuthenticationTypesAsync();
if (types.includes(LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION)) {
setBiometricType('facial');
} else if (types.includes(LocalAuthentication.AuthenticationType.FINGERPRINT)) {
setBiometricType('fingerprint');
} else if (types.includes(LocalAuthentication.AuthenticationType.IRIS)) {
setBiometricType('iris');
}
}
} catch (error) {
if (__DEV__) console.error('Biometric init error:', error);
}
}, []);
/**
* Load settings from LOCAL STORAGE ONLY
* Data never leaves the device
*/
const loadSettings = React.useCallback(async () => {
try {
// Load biometric enabled status (local only)
const enabled = await AsyncStorage.getItem(BIOMETRIC_ENABLED_KEY);
setIsBiometricEnabled(enabled === 'true');
// Load auto-lock timer (local only)
const timer = await AsyncStorage.getItem(AUTO_LOCK_TIMER_KEY);
if (timer) {
setAutoLockTimerState(parseInt(timer, 10));
}
// Check if app should be locked
await checkAutoLock();
} catch (error) {
if (__DEV__) console.error('Error loading settings:', error);
}
}, [checkAutoLock]);
useEffect(() => {
// Initialize biometric and load settings on mount
// eslint-disable-next-line react-hooks/set-state-in-effect
initBiometric();
loadSettings();
}, [initBiometric, loadSettings]);
/**
* Authenticate using biometric
* Authentication happens ON DEVICE ONLY
*/
const authenticate = async (): Promise<boolean> => {
try {
if (!isBiometricSupported || !isBiometricEnrolled) {
return false;
}
const result = await LocalAuthentication.authenticateAsync({
promptMessage: 'Authenticate to unlock PezkuwiChain',
cancelLabel: 'Cancel',
disableDeviceFallback: false,
fallbackLabel: 'Use PIN',
});
if (result.success) {
unlock();
return true;
}
return false;
} catch (error) {
if (__DEV__) console.error('Authentication error:', error);
return false;
}
};
/**
* Enable biometric authentication
* Settings saved LOCALLY ONLY
*/
const enableBiometric = async (): Promise<boolean> => {
try {
// First authenticate to enable
const authenticated = await authenticate();
if (authenticated) {
// Save enabled status LOCALLY
await AsyncStorage.setItem(BIOMETRIC_ENABLED_KEY, 'true');
setIsBiometricEnabled(true);
return true;
}
return false;
} catch (error) {
if (__DEV__) console.error('Enable biometric error:', error);
return false;
}
};
/**
* Disable biometric authentication
* Settings saved LOCALLY ONLY
*/
const disableBiometric = async (): Promise<void> => {
try {
await AsyncStorage.setItem(BIOMETRIC_ENABLED_KEY, 'false');
setIsBiometricEnabled(false);
} catch (error) {
if (__DEV__) console.error('Disable biometric error:', error);
}
};
/**
* Set PIN code as backup
* PIN stored in ENCRYPTED SECURE STORE on device
* NEVER sent to server
*/
const setPinCode = async (pin: string): Promise<void> => {
try {
// Hash the PIN before storing (simple hash for demo)
// In production, use proper cryptographic hashing
const hashedPin = await hashPin(pin);
// Store in SecureStore (encrypted on device)
await SecureStore.setItemAsync(PIN_CODE_KEY, hashedPin);
} catch (error) {
if (__DEV__) console.error('Set PIN error:', error);
throw error;
}
};
/**
* Verify PIN code
* Verification happens LOCALLY on device
*/
const verifyPinCode = async (pin: string): Promise<boolean> => {
try {
// Get stored PIN from SecureStore (local encrypted storage)
const storedPin = await SecureStore.getItemAsync(PIN_CODE_KEY);
if (!storedPin) {
return false;
}
// Hash entered PIN
const hashedPin = await hashPin(pin);
// Compare (happens on device)
if (hashedPin === storedPin) {
unlock();
return true;
}
return false;
} catch (error) {
if (__DEV__) console.error('Verify PIN error:', error);
return false;
}
};
/**
* Simple PIN hashing (for demo)
* In production, use bcrypt or similar
* All happens ON DEVICE
*/
const hashPin = async (pin: string): Promise<string> => {
// Simple hash for demo - replace with proper crypto in production
let hash = 0;
for (let i = 0; i < pin.length; i++) {
const char = pin.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return hash.toString();
};
/**
* Set auto-lock timer
* Setting saved LOCALLY ONLY
*/
const setAutoLockTimer = async (minutes: number): Promise<void> => {
try {
await AsyncStorage.setItem(AUTO_LOCK_TIMER_KEY, minutes.toString());
setAutoLockTimerState(minutes);
} catch (error) {
if (__DEV__) console.error('Set auto-lock timer error:', error);
}
};
/**
* Lock the app
* State change is LOCAL ONLY
*/
const lock = () => {
setIsLocked(true);
};
/**
* Unlock the app
* Saves timestamp LOCALLY for auto-lock
*/
const unlock = () => {
setIsLocked(false);
// Save unlock time LOCALLY for auto-lock check (async without await)
AsyncStorage.setItem(LAST_UNLOCK_TIME_KEY, Date.now().toString()).catch((error) => {
if (__DEV__) console.error('Save unlock time error:', error);
});
};
return (
<BiometricAuthContext.Provider
value={{
isBiometricSupported,
isBiometricEnrolled,
isBiometricAvailable,
biometricType,
isBiometricEnabled,
isLocked,
autoLockTimer,
authenticate,
enableBiometric,
disableBiometric,
setPinCode,
verifyPinCode,
setAutoLockTimer,
lock,
unlock,
checkAutoLock,
}}
>
{children}
</BiometricAuthContext.Provider>
);
};
export const useBiometricAuth = () => {
const context = useContext(BiometricAuthContext);
if (context === undefined) {
throw new Error('useBiometricAuth must be used within BiometricAuthProvider');
}
return context;
};