From 3a15142940ef9391868442e1997c3d9d5eec33be Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Thu, 15 Sep 2022 23:41:03 +0530 Subject: [PATCH] fix: biometrics input on mobile webview challenge modal (#1572) --- packages/mobile/src/Lib/Interface.ts | 47 +++++++++++++++++ .../Domain/Device/MobileDeviceInterface.ts | 1 + .../ChallengeModal/ChallengeModal.tsx | 20 ++++++-- .../ChallengeModal/ChallengePrompt.tsx | 51 +++++++++++++++++-- 4 files changed, 111 insertions(+), 8 deletions(-) diff --git a/packages/mobile/src/Lib/Interface.ts b/packages/mobile/src/Lib/Interface.ts index f3a02ac41..4e6931064 100644 --- a/packages/mobile/src/Lib/Interface.ts +++ b/packages/mobile/src/Lib/Interface.ts @@ -288,6 +288,53 @@ export class MobileDeviceInterface implements DeviceInterface { } } + authenticateWithBiometrics() { + return new Promise((resolve) => { + if (Platform.OS === 'android') { + FingerprintScanner.authenticate({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore ts type does not exist for deviceCredentialAllowed + deviceCredentialAllowed: true, + description: 'Biometrics are required to access your notes.', + }) + .then(() => { + FingerprintScanner.release() + resolve(true) + }) + .catch((error) => { + FingerprintScanner.release() + if (error.name === 'DeviceLocked') { + Alert.alert('Unsuccessful', 'Authentication failed. Wait 30 seconds to try again.') + } else { + Alert.alert('Unsuccessful', 'Authentication failed. Tap to try again.') + } + resolve(false) + }) + } else { + // iOS + FingerprintScanner.authenticate({ + fallbackEnabled: true, + description: 'This is required to access your notes.', + }) + .then(() => { + FingerprintScanner.release() + resolve(true) + }) + .catch((error_1) => { + FingerprintScanner.release() + if (error_1.name !== 'SystemCancel') { + if (error_1.name !== 'UserCancel') { + Alert.alert('Unsuccessful') + } else { + Alert.alert('Unsuccessful', 'Authentication failed. Tap to try again.') + } + } + resolve(false) + }) + } + }) + } + getRawKeychainValue(): Promise { return Keychain.getKeys() } diff --git a/packages/services/src/Domain/Device/MobileDeviceInterface.ts b/packages/services/src/Domain/Device/MobileDeviceInterface.ts index 68d2d9061..87315064b 100644 --- a/packages/services/src/Domain/Device/MobileDeviceInterface.ts +++ b/packages/services/src/Domain/Device/MobileDeviceInterface.ts @@ -9,4 +9,5 @@ export interface MobileDeviceInterface extends DeviceInterface { setAndroidScreenshotPrivacy(enable: boolean): Promise getMobileScreenshotPrivacyEnabled(): Promise setMobileScreenshotPrivacyEnabled(isEnabled: boolean): Promise + authenticateWithBiometrics(): Promise } diff --git a/packages/web/src/javascripts/Components/ChallengeModal/ChallengeModal.tsx b/packages/web/src/javascripts/Components/ChallengeModal/ChallengeModal.tsx index e8b3356c3..a00043a5b 100644 --- a/packages/web/src/javascripts/Components/ChallengeModal/ChallengeModal.tsx +++ b/packages/web/src/javascripts/Components/ChallengeModal/ChallengeModal.tsx @@ -5,6 +5,7 @@ import { Challenge, ChallengePrompt, ChallengeReason, + ChallengeValidation, ChallengeValue, removeFromArray, } from '@standardnotes/snjs' @@ -17,6 +18,7 @@ import LockscreenWorkspaceSwitcher from './LockscreenWorkspaceSwitcher' import { ApplicationGroup } from '@/Application/ApplicationGroup' import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import { ChallengeModalValues } from './ChallengeModalValues' +import { InputValue } from './InputValue' type Props = { application: WebApplication @@ -64,9 +66,11 @@ const ChallengeModal: FunctionComponent = ({ const [isProcessing, setIsProcessing] = useState(false) const [, setProcessingPrompts] = useState([]) const [bypassModalFocusLock, setBypassModalFocusLock] = useState(false) + const shouldShowForgotPasscode = [ChallengeReason.ApplicationUnlock, ChallengeReason.Migration].includes( challenge.reason, ) + const shouldShowWorkspaceSwitcher = challenge.reason === ChallengeReason.ApplicationUnlock const submit = useCallback(() => { @@ -106,7 +110,7 @@ const ChallengeModal: FunctionComponent = ({ }, [application, challenge, isProcessing, isSubmitting, values]) const onValueChange = useCallback( - (value: string | number, prompt: ChallengePrompt) => { + (value: InputValue['value'], prompt: ChallengePrompt) => { const newValues = { ...values } newValues[prompt.id].invalid = false newValues[prompt.id].value = value @@ -169,6 +173,17 @@ const ChallengeModal: FunctionComponent = ({ } }, [application, challenge, onDismiss]) + const biometricPrompt = challenge.prompts.find((prompt) => prompt.validation === ChallengeValidation.Biometric) + const hasOnlyBiometricPrompt = challenge.prompts.length === 1 && !!biometricPrompt + const hasBiometricPromptValue = biometricPrompt && values[biometricPrompt.id].value + + useEffect(() => { + const shouldAutoSubmit = hasOnlyBiometricPrompt && hasBiometricPromptValue + if (shouldAutoSubmit) { + submit() + } + }, [hasBiometricPromptValue, hasOnlyBiometricPrompt, submit]) + if (!challenge.prompts) { return null } @@ -201,11 +216,9 @@ const ChallengeModal: FunctionComponent = ({ )}
{challenge.heading}
- {challenge.subheading && (
{challenge.subheading}
)} -
{ @@ -215,6 +228,7 @@ const ChallengeModal: FunctionComponent = ({ > {challenge.prompts.map((prompt, index) => ( void + onValueChange: (value: InputValue['value'], prompt: ChallengePrompt) => void isInvalid: boolean } -const ChallengeModalPrompt: FunctionComponent = ({ prompt, values, index, onValueChange, isInvalid }) => { +const ChallengeModalPrompt: FunctionComponent = ({ + application, + prompt, + values, + index, + onValueChange, + isInvalid, +}) => { const inputRef = useRef(null) + const biometricsButtonRef = useRef(null) useEffect(() => { - if (index === 0) { + const isNotFirstPrompt = index !== 0 + + if (isNotFirstPrompt) { + return + } + + if (prompt.validation === ChallengeValidation.Biometric) { + biometricsButtonRef.current?.click() + } else { inputRef.current?.focus() } - }, [index]) + }, [index, prompt.validation]) useEffect(() => { if (isInvalid) { @@ -61,6 +86,22 @@ const ChallengeModalPrompt: FunctionComponent = ({ prompt, values, index, })} + ) : prompt.validation === ChallengeValidation.Biometric ? ( +
+ +
) : prompt.secureTextEntry ? (