mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-02-15 08:42:10 -05:00
Allow biometric and PIN unlock options to be enabled at the same time (#1562)
This commit is contained in:
committed by
Leendert de Borst
parent
b9791a0563
commit
d2e7e29b50
@@ -133,16 +133,10 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode {
|
||||
return;
|
||||
}
|
||||
|
||||
// If enabling biometrics and PIN is enabled, disable PIN first
|
||||
if (value && pinEnabled) {
|
||||
try {
|
||||
await NativeVaultManager.removeAndDisablePin();
|
||||
setPinEnabled(false);
|
||||
} catch (error) {
|
||||
console.error('Failed to disable PIN:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Biometrics and PIN can now both be enabled simultaneously.
|
||||
* Biometrics takes priority during unlock, PIN serves as fallback.
|
||||
*/
|
||||
setIsBiometricsEnabled(value);
|
||||
setAuthMethods(value ? ['faceid', 'password'] : ['password']);
|
||||
|
||||
@@ -155,7 +149,7 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode {
|
||||
visibilityTime: 1200,
|
||||
});
|
||||
}
|
||||
}, [hasBiometrics, pinEnabled, setAuthMethods, biometricDisplayName, showDialog, t]);
|
||||
}, [hasBiometrics, setAuthMethods, biometricDisplayName, showDialog, t]);
|
||||
|
||||
/**
|
||||
* Handle enable PIN - launches native PIN setup UI.
|
||||
@@ -165,12 +159,10 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode {
|
||||
// Launch native PIN setup UI
|
||||
await NativeVaultManager.showPinSetup();
|
||||
|
||||
// PIN setup successful - now disable biometrics if it was enabled
|
||||
if (isBiometricsEnabled) {
|
||||
setIsBiometricsEnabled(false);
|
||||
await setAuthMethods(['password']);
|
||||
}
|
||||
|
||||
/*
|
||||
* PIN and biometrics can now both be enabled simultaneously.
|
||||
* Biometrics takes priority during unlock, PIN serves as fallback.
|
||||
*/
|
||||
setPinEnabled(true);
|
||||
Toast.show({
|
||||
type: 'success',
|
||||
@@ -188,7 +180,7 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode {
|
||||
console.error('Failed to enable PIN:', error);
|
||||
showAlert(t('common.error'), t('common.errors.unknownErrorTryAgain'));
|
||||
}
|
||||
}, [isBiometricsEnabled, setAuthMethods, showAlert, t]);
|
||||
}, [showAlert, t]);
|
||||
|
||||
/**
|
||||
* Handle disable PIN.
|
||||
|
||||
@@ -168,10 +168,78 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the biometrics retry.
|
||||
* Internal PIN unlock handler - performs PIN unlock and navigates on success.
|
||||
* Returns true if unlock succeeded, false otherwise.
|
||||
*/
|
||||
const handleUnlockRetry = async () : Promise<void> => {
|
||||
router.replace('/reinitialize');
|
||||
const performPinUnlock = async () : Promise<boolean> => {
|
||||
try {
|
||||
await NativeVaultManager.showPinUnlock();
|
||||
|
||||
// Check if vault is now unlocked
|
||||
const isNowUnlocked = await NativeVaultManager.isVaultUnlocked();
|
||||
if (isNowUnlocked) {
|
||||
// Check if the vault is up to date
|
||||
if (await dbContext.hasPendingMigrations()) {
|
||||
router.replace('/upgrade');
|
||||
return true;
|
||||
}
|
||||
router.replace('/reinitialize');
|
||||
return true;
|
||||
}
|
||||
// Not unlocked means user cancelled - return false but don't show error
|
||||
return false;
|
||||
} catch (err) {
|
||||
// User cancelled or PIN unlock failed
|
||||
const errorMessage = err instanceof Error ? err.message : '';
|
||||
if (!errorMessage.includes('cancelled') && !errorMessage.includes('canceled')) {
|
||||
console.error('PIN unlock error:', err);
|
||||
setError(t('auth.errors.pinFailed'));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the biometrics retry - directly triggers biometric unlock.
|
||||
* If biometric fails and PIN is available, automatically falls back to PIN.
|
||||
*/
|
||||
const handleBiometricRetry = async () : Promise<void> => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const unlocked = await dbContext.unlockVault();
|
||||
if (unlocked) {
|
||||
// Check if the vault is up to date
|
||||
if (await dbContext.hasPendingMigrations()) {
|
||||
router.replace('/upgrade');
|
||||
return;
|
||||
}
|
||||
router.replace('/reinitialize');
|
||||
} else {
|
||||
// Biometric failed - try PIN fallback if available
|
||||
if (pinAvailable) {
|
||||
await performPinUnlock();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Biometric retry error:', err);
|
||||
// Biometric failed - try PIN fallback if available
|
||||
if (pinAvailable) {
|
||||
await performPinUnlock();
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the PIN retry button - directly triggers PIN unlock flow.
|
||||
*/
|
||||
const handlePinRetry = async () : Promise<void> => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
await performPinUnlock();
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
@@ -396,7 +464,7 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
{isBiometricsAvailable && (
|
||||
<RobustPressable
|
||||
style={styles.faceIdButton}
|
||||
onPress={handleUnlockRetry}
|
||||
onPress={handleBiometricRetry}
|
||||
>
|
||||
<ThemedText style={styles.faceIdButtonText}>{t('auth.tryBiometricAgain', { biometric: biometricDisplayName })}</ThemedText>
|
||||
</RobustPressable>
|
||||
@@ -406,7 +474,7 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
{pinAvailable && (
|
||||
<RobustPressable
|
||||
style={styles.faceIdButton}
|
||||
onPress={handleUnlockRetry}
|
||||
onPress={handlePinRetry}
|
||||
>
|
||||
<ThemedText style={styles.faceIdButtonText}>{t('auth.tryPinAgain')}</ThemedText>
|
||||
</RobustPressable>
|
||||
|
||||
@@ -15,7 +15,8 @@ export type UnlockResult = {
|
||||
export class VaultUnlockHelper {
|
||||
/**
|
||||
* Attempt to unlock the vault using available authentication methods.
|
||||
* Tries biometric first (if available), then PIN (if enabled), otherwise indicates manual unlock needed.
|
||||
* Priority: Biometric -> PIN -> Manual unlock
|
||||
* If biometric fails/is cancelled and PIN is enabled, automatically falls back to PIN.
|
||||
*
|
||||
* @param params Configuration for unlock attempt
|
||||
* @returns Promise<UnlockResult> indicating success/failure and any actions needed
|
||||
@@ -34,52 +35,32 @@ export class VaultUnlockHelper {
|
||||
if (isFaceIDEnabled) {
|
||||
try {
|
||||
const isUnlocked = await unlockVault();
|
||||
if (!isUnlocked) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Biometric unlock failed',
|
||||
redirectToUnlock: true,
|
||||
};
|
||||
if (isUnlocked) {
|
||||
return { success: true };
|
||||
}
|
||||
return { success: true };
|
||||
// Biometric failed - fall through to PIN fallback below
|
||||
console.log('Biometric unlock returned false, trying PIN fallback if available');
|
||||
} catch (error) {
|
||||
// Biometric error - fall through to PIN fallback below
|
||||
console.error('Biometric unlock error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Biometric unlock failed',
|
||||
redirectToUnlock: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Biometric failed or was cancelled - try PIN fallback if available
|
||||
if (isPinEnabled) {
|
||||
return this.attemptPinUnlock();
|
||||
}
|
||||
|
||||
// No PIN fallback available
|
||||
return {
|
||||
success: false,
|
||||
error: 'Biometric unlock failed',
|
||||
redirectToUnlock: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Try PIN unlock if biometric is not available
|
||||
// Biometric not enabled - try PIN unlock directly
|
||||
if (isPinEnabled) {
|
||||
try {
|
||||
await NativeVaultManager.showPinUnlock();
|
||||
|
||||
// Verify vault is now unlocked
|
||||
const isNowUnlocked = await NativeVaultManager.isVaultUnlocked();
|
||||
if (!isNowUnlocked) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'PIN unlock failed',
|
||||
redirectToUnlock: true,
|
||||
};
|
||||
}
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
// User cancelled or PIN unlock failed
|
||||
// Only log non-cancellation errors to reduce noise
|
||||
const errorMessage = error instanceof Error ? error.message : 'PIN unlock failed or cancelled';
|
||||
if (!errorMessage.includes('cancelled')) {
|
||||
console.error('PIN unlock error:', error);
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
redirectToUnlock: true,
|
||||
};
|
||||
}
|
||||
return this.attemptPinUnlock();
|
||||
}
|
||||
|
||||
// No automatic unlock method available - manual unlock required
|
||||
@@ -90,6 +71,38 @@ export class VaultUnlockHelper {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt PIN unlock.
|
||||
* @returns Promise<UnlockResult> indicating success/failure
|
||||
*/
|
||||
private static async attemptPinUnlock(): Promise<UnlockResult> {
|
||||
try {
|
||||
await NativeVaultManager.showPinUnlock();
|
||||
|
||||
// Verify vault is now unlocked
|
||||
const isNowUnlocked = await NativeVaultManager.isVaultUnlocked();
|
||||
if (!isNowUnlocked) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'PIN unlock failed',
|
||||
redirectToUnlock: true,
|
||||
};
|
||||
}
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
// User cancelled or PIN unlock failed
|
||||
const errorMessage = error instanceof Error ? error.message : 'PIN unlock failed or cancelled';
|
||||
if (!errorMessage.includes('cancelled') && !errorMessage.includes('canceled')) {
|
||||
console.error('PIN unlock error:', error);
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
redirectToUnlock: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate user for a specific action (e.g., mobile unlock confirmation).
|
||||
* Uses the native authenticateUser which automatically detects and uses the appropriate method.
|
||||
|
||||
Reference in New Issue
Block a user