mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-14 18:35:16 -04:00
Tweak re-authenticate flow with custom title/subtitle (#1347)
This commit is contained in:
@@ -143,7 +143,7 @@ export default function SettingsLayout(): React.ReactNode {
|
||||
<Stack.Screen
|
||||
name="qr-result"
|
||||
options={{
|
||||
title: t('settings.qrScanner.mobileUnlock.successTitle'),
|
||||
title: t('common.success'),
|
||||
...defaultHeaderOptions,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -103,56 +103,43 @@ export default function QRConfirmScreen() : React.ReactNode {
|
||||
setIsProcessing(true);
|
||||
|
||||
try {
|
||||
let authenticated = false;
|
||||
// Check if biometric or PIN is enabled
|
||||
const authMethods = await NativeVaultManager.getAuthMethods();
|
||||
const isPinEnabled = await NativeVaultManager.isPinEnabled();
|
||||
const isBiometricEnabled = authMethods.includes('faceid');
|
||||
|
||||
// Check which authentication method is available
|
||||
const pinEnabled = await NativeVaultManager.isPinEnabled();
|
||||
|
||||
if (pinEnabled) {
|
||||
// PIN is enabled, use PIN unlock
|
||||
try {
|
||||
await NativeVaultManager.showPinUnlock();
|
||||
authenticated = true;
|
||||
} catch (pinError: unknown) {
|
||||
// User cancelled PIN or PIN failed
|
||||
console.error('PIN unlock cancelled or failed:', pinError);
|
||||
Alert.alert(
|
||||
t('common.error'),
|
||||
t('settings.qrScanner.mobileUnlock.authenticationFailed')
|
||||
);
|
||||
setIsProcessing(false);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Try biometric authentication
|
||||
try {
|
||||
authenticated = await NativeVaultManager.authenticateUser(
|
||||
t('settings.qrScanner.mobileUnlock.authenticationRequired')
|
||||
);
|
||||
} catch (authError: unknown) {
|
||||
console.error('Biometric authentication error:', authError);
|
||||
Alert.alert(
|
||||
t('common.error'),
|
||||
t('settings.qrScanner.mobileUnlock.authenticationFailed')
|
||||
);
|
||||
setIsProcessing(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!authenticated) {
|
||||
if (!isBiometricEnabled && !isPinEnabled) {
|
||||
Alert.alert(
|
||||
t('common.error'),
|
||||
t('settings.qrScanner.mobileUnlock.authenticationFailed')
|
||||
t('settings.qrScanner.mobileUnlock.noAuthMethodEnabled'),
|
||||
[
|
||||
{
|
||||
text: t('common.ok'),
|
||||
onPress: (): void => {
|
||||
router.back();
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
setIsProcessing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Authenticate user with either biometric or PIN (automatically detected)
|
||||
const authenticated = await NativeVaultManager.authenticateUser(
|
||||
t('settings.qrScanner.mobileUnlock.confirmTitle'),
|
||||
t('settings.qrScanner.mobileUnlock.confirmSubtitle')
|
||||
);
|
||||
|
||||
if (!authenticated) {
|
||||
setIsProcessing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Process the mobile unlock
|
||||
await handleMobileUnlock(requestId);
|
||||
} catch (error) {
|
||||
console.error('QR code processing error:', error);
|
||||
console.error('Authentication or QR code processing error:', error);
|
||||
Alert.alert(
|
||||
t('common.error'),
|
||||
error instanceof Error ? error.message : t('common.errors.unknownError')
|
||||
|
||||
@@ -88,7 +88,7 @@ export default function QRResultScreen() : React.ReactNode {
|
||||
/>
|
||||
<ThemedText style={styles.title}>
|
||||
{isSuccess
|
||||
? t('settings.qrScanner.mobileUnlock.successTitle')
|
||||
? t('common.success')
|
||||
: t('common.error')}
|
||||
</ThemedText>
|
||||
<ThemedText style={styles.message}>
|
||||
|
||||
@@ -404,13 +404,13 @@
|
||||
"cameraPermissionTitle": "Camera Permission Required",
|
||||
"cameraPermissionMessage": "Please allow camera access to scan QR codes.",
|
||||
"mobileUnlock": {
|
||||
"confirmTitle": "Confirm Login",
|
||||
"confirmTitle": "Confirm Login Request",
|
||||
"confirmSubtitle": "Re-authenticate to approve login on another device.",
|
||||
"confirmMessage": "You are about to log in on a remote device with your account. This other device will have full access to your vault. Only proceed if you trust this device.",
|
||||
"successTitle": "Device Unlocked",
|
||||
"successDescription": "The remote device has been successfully unlocked.",
|
||||
"requestExpired": "This unlock request has expired. Please generate a new QR code.",
|
||||
"successDescription": "The remote device has been successfully logged in.",
|
||||
"requestExpired": "This login request has expired. Please generate a new QR code.",
|
||||
"authenticationFailed": "Authentication failed. Please try again.",
|
||||
"authenticationRequired": "Authentication required to confirm this action."
|
||||
"noAuthMethodEnabled": "Biometric or PIN unlock needs to be enabled to unlock with mobile"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -281,8 +281,8 @@
|
||||
|
||||
// MARK: - Re-authentication
|
||||
|
||||
- (void)authenticateUser:(NSString *)reason resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
||||
[vaultManager authenticateUser:reason resolver:resolve rejecter:reject];
|
||||
- (void)authenticateUser:(NSString *)title subtitle:(NSString *)subtitle resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
||||
[vaultManager authenticateUser:title subtitle:subtitle resolver:resolve rejecter:reject];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -904,14 +904,69 @@ public class VaultManager: NSObject {
|
||||
}
|
||||
|
||||
@objc
|
||||
func authenticateUser(_ reason: String,
|
||||
func authenticateUser(_ title: String?,
|
||||
subtitle: String?,
|
||||
resolver resolve: @escaping RCTPromiseResolveBlock,
|
||||
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
||||
do {
|
||||
let authenticated = try vaultStore.authenticateUser(reason: reason)
|
||||
// Check if PIN is enabled first
|
||||
let pinEnabled = vaultStore.isPinEnabled()
|
||||
|
||||
if pinEnabled {
|
||||
// PIN is enabled, show PIN unlock UI
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else {
|
||||
reject("INTERNAL_ERROR", "VaultManager instance deallocated", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the root view controller from React Native
|
||||
guard let rootVC = RCTPresentedViewController() else {
|
||||
reject("NO_VIEW_CONTROLLER", "No view controller available", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Create PIN unlock view with ViewModel
|
||||
// Use custom title/subtitle if provided, otherwise use defaults
|
||||
let customTitle = (title?.isEmpty == false) ? title : nil
|
||||
let customSubtitle = (subtitle?.isEmpty == false) ? subtitle : nil
|
||||
let viewModel = PinUnlockViewModel(
|
||||
pinLength: self.vaultStore.getPinLength(),
|
||||
customTitle: customTitle,
|
||||
customSubtitle: customSubtitle,
|
||||
unlockHandler: { [weak self] pin in
|
||||
guard let self = self else {
|
||||
throw NSError(domain: "VaultManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "VaultManager instance deallocated"])
|
||||
}
|
||||
|
||||
// Unlock vault with PIN (just validates, doesn't store in memory)
|
||||
_ = try self.vaultStore.unlockWithPin(pin)
|
||||
|
||||
// Success - dismiss and resolve
|
||||
await MainActor.run {
|
||||
rootVC.dismiss(animated: true) {
|
||||
resolve(true)
|
||||
}
|
||||
}
|
||||
},
|
||||
cancelHandler: {
|
||||
// User cancelled - dismiss and resolve with false
|
||||
rootVC.dismiss(animated: true) {
|
||||
resolve(false)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
let pinView = PinUnlockView(viewModel: viewModel)
|
||||
let hostingController = UIHostingController(rootView: pinView)
|
||||
|
||||
// Present modally as full screen
|
||||
hostingController.modalPresentationStyle = .fullScreen
|
||||
rootVC.present(hostingController, animated: true)
|
||||
}
|
||||
} else {
|
||||
// Use biometric authentication
|
||||
let authenticated = vaultStore.authenticateUser(title: title, subtitle: subtitle)
|
||||
resolve(authenticated)
|
||||
} catch {
|
||||
reject("AUTH_ERROR", "Authentication failed: \(error.localizedDescription)", error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,17 +42,26 @@ extension VaultStore {
|
||||
return self.enabledAuthMethods
|
||||
}
|
||||
|
||||
/// Authenticate the user using biometric authentication
|
||||
/// Authenticate the user using biometric authentication only
|
||||
/// Note: This method only handles biometric authentication. If PIN is enabled,
|
||||
/// this will return false and the caller should use showPinUnlock instead.
|
||||
/// Returns true if authentication succeeded, false otherwise
|
||||
public func authenticateUser(reason: String) throws -> Bool {
|
||||
/// - Parameter title: The title for authentication. Optional, defaults to "Unlock Vault" context.
|
||||
/// - Parameter subtitle: The subtitle for authentication. Optional, defaults to title or "Unlock Vault" context.
|
||||
public func authenticateUser(title: String?, subtitle: String?) -> Bool {
|
||||
// Use title if provided, otherwise default
|
||||
let authReason = (title?.isEmpty == false) ? title! : "Unlock Vault"
|
||||
|
||||
// Check if PIN is enabled - if so, return false (caller should use PIN UI)
|
||||
if isPinEnabled() {
|
||||
print("PIN authentication is enabled, returning false")
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if biometric authentication is enabled
|
||||
guard self.enabledAuthMethods.contains(.faceID) else {
|
||||
print("Biometric authentication not enabled")
|
||||
throw NSError(
|
||||
domain: "VaultStore",
|
||||
code: 100,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Biometric authentication not enabled"]
|
||||
)
|
||||
print("No authentication method enabled")
|
||||
return false
|
||||
}
|
||||
|
||||
let context = LAContext()
|
||||
@@ -61,11 +70,7 @@ extension VaultStore {
|
||||
// Check if biometric authentication is available
|
||||
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
|
||||
print("Biometric authentication not available: \(error?.localizedDescription ?? "unknown error")")
|
||||
throw NSError(
|
||||
domain: "VaultStore",
|
||||
code: 100,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Biometric authentication not available"]
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
// Perform biometric authentication synchronously
|
||||
@@ -74,7 +79,7 @@ extension VaultStore {
|
||||
|
||||
context.evaluatePolicy(
|
||||
.deviceOwnerAuthenticationWithBiometrics,
|
||||
localizedReason: reason
|
||||
localizedReason: authReason
|
||||
) { success, authError in
|
||||
authenticated = success
|
||||
if let authError = authError {
|
||||
|
||||
@@ -46,13 +46,13 @@ public struct PinUnlockView: View {
|
||||
.padding(.bottom, 12)
|
||||
|
||||
// Title
|
||||
Text(String(localized: "unlock_vault", bundle: locBundle))
|
||||
Text(viewModel.customTitle ?? String(localized: "unlock_vault", bundle: locBundle))
|
||||
.font(.system(size: 22, weight: .semibold))
|
||||
.foregroundColor(colors.text)
|
||||
.padding(.bottom, 6)
|
||||
|
||||
// Subtitle
|
||||
Text(String(format: String(localized: "enter_pin_to_unlock_vault", bundle: locBundle)))
|
||||
Text(viewModel.customSubtitle ?? String(format: String(localized: "enter_pin_to_unlock_vault", bundle: locBundle)))
|
||||
.font(.system(size: 15))
|
||||
.foregroundColor(colors.text.opacity(0.7))
|
||||
.multilineTextAlignment(.center)
|
||||
@@ -219,15 +219,21 @@ public class PinUnlockViewModel: ObservableObject {
|
||||
@Published public var isUnlocking: Bool = false
|
||||
|
||||
public let pinLength: Int?
|
||||
public let customTitle: String?
|
||||
public let customSubtitle: String?
|
||||
private let unlockHandler: (String) async throws -> Void
|
||||
private let cancelHandler: () -> Void
|
||||
|
||||
public init(
|
||||
pinLength: Int?,
|
||||
customTitle: String? = nil,
|
||||
customSubtitle: String? = nil,
|
||||
unlockHandler: @escaping (String) async throws -> Void,
|
||||
cancelHandler: @escaping () -> Void
|
||||
) {
|
||||
self.pinLength = pinLength
|
||||
self.customTitle = customTitle
|
||||
self.customSubtitle = customSubtitle
|
||||
self.unlockHandler = unlockHandler
|
||||
self.cancelHandler = cancelHandler
|
||||
}
|
||||
|
||||
@@ -100,7 +100,8 @@ export interface Spec extends TurboModule {
|
||||
encryptDecryptionKeyForMobileUnlock(publicKeyJWK: string): Promise<string>;
|
||||
|
||||
// Re-authentication methods
|
||||
authenticateUser(reason: string): Promise<boolean>;
|
||||
// Authenticate user with biometric or PIN. If title/subtitle are null/empty, defaults to "Unlock Vault" context.
|
||||
authenticateUser(title: string | null, subtitle: string | null): Promise<boolean>;
|
||||
}
|
||||
|
||||
export default TurboModuleRegistry.getEnforcing<Spec>('NativeVaultManager');
|
||||
|
||||
Reference in New Issue
Block a user