Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/__tests__/services/mobileAuth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ import * as LocalAuthentication from 'expo-local-authentication';
import apiClient from '../../services/api/axios.config';
import {
BiometricReenrollmentError,
isValidBiometricType,
mobileAuthService,
VALID_BIOMETRIC_TYPES,
} from '../../services/mobileAuth';
import * as secureStorage from '../../services/secureStorage';

Expand Down Expand Up @@ -587,4 +589,28 @@ describe('MobileAuthService — Biometric Re-enrollment Flow', () => {
expect(result2).toEqual(MOCK_AUTH_RESULT);
});
});

describe('isValidBiometricType', () => {
it.each(VALID_BIOMETRIC_TYPES)('should accept the valid biometric type "%s"', (type) => {
expect(isValidBiometricType(type)).toBe(true);
});

it.each([
'retina',
'voice',
'Fingerprint',
'',
' ',
'fingerprint ',
])('should reject the invalid string "%s"', (value) => {
expect(isValidBiometricType(value)).toBe(false);
});

it.each([null, undefined, 1, true, {}, [], Symbol('face')])(
'should reject the non-string value %p',
(value) => {
expect(isValidBiometricType(value)).toBe(false);
}
);
});
});
23 changes: 20 additions & 3 deletions src/services/mobileAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,18 @@ export interface SocialProvider {

export type BiometricType = 'fingerprint' | 'face' | 'iris' | 'none';

export const VALID_BIOMETRIC_TYPES: readonly BiometricType[] = ['fingerprint', 'face', 'iris', 'none'];

/**
* Runtime type guard for BiometricType. Needed because biometric type
* values can come from mockable/abstract native modules, so a value
* claiming to be a BiometricType at compile time is not guaranteed to
* actually be one at runtime (e.g. in a mocked test environment).
*/
export function isValidBiometricType(value: unknown): value is BiometricType {
return typeof value === 'string' && (VALID_BIOMETRIC_TYPES as readonly string[]).includes(value);
}

// ─── Biometric re-enrollment error ────────────────────────────────────────────

/**
Expand Down Expand Up @@ -293,13 +305,18 @@ class MobileAuthService {

// expo-local-authentication returns an array of SupportedAuthenticationTypes
// enum values. We map the first supported type to our BiometricType.
if (types.includes(1)) {
const resolvedType: BiometricType = types.includes(1)
// BIOMETRIC = 1 (fingerprint, face, etc.)
// On iOS we can't distinguish face vs fingerprint from this enum,
// so we default to 'fingerprint' and let the UI adapt.
return 'fingerprint';
? 'fingerprint'
: 'none';

if (!isValidBiometricType(resolvedType)) {
throw new Error(`Invalid biometric type resolved: ${String(resolvedType)}`);
}
return 'none';

return resolvedType;
} catch {
return 'none';
}
Expand Down
Loading