fix: biometrics input on mobile webview challenge modal (#1572)

This commit is contained in:
Aman Harwara
2022-09-15 23:41:03 +05:30
committed by GitHub
parent 79518b6a5d
commit 3a15142940
4 changed files with 111 additions and 8 deletions

View File

@@ -288,6 +288,53 @@ export class MobileDeviceInterface implements DeviceInterface {
}
}
authenticateWithBiometrics() {
return new Promise<boolean>((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<RawKeychainValue | null | undefined> {
return Keychain.getKeys()
}

View File

@@ -9,4 +9,5 @@ export interface MobileDeviceInterface extends DeviceInterface {
setAndroidScreenshotPrivacy(enable: boolean): Promise<void>
getMobileScreenshotPrivacyEnabled(): Promise<boolean | undefined>
setMobileScreenshotPrivacyEnabled(isEnabled: boolean): Promise<void>
authenticateWithBiometrics(): Promise<boolean>
}

View File

@@ -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<Props> = ({
const [isProcessing, setIsProcessing] = useState(false)
const [, setProcessingPrompts] = useState<ChallengePrompt[]>([])
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<Props> = ({
}, [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<Props> = ({
}
}, [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<Props> = ({
)}
<ProtectedIllustration className="mb-4 h-30 w-30" />
<div className="mb-3 max-w-76 text-center text-lg font-bold">{challenge.heading}</div>
{challenge.subheading && (
<div className="break-word mb-4 max-w-76 text-center text-sm">{challenge.subheading}</div>
)}
<form
className="flex min-w-76 flex-col items-center"
onSubmit={(e) => {
@@ -215,6 +228,7 @@ const ChallengeModal: FunctionComponent<Props> = ({
>
{challenge.prompts.map((prompt, index) => (
<ChallengeModalPrompt
application={application}
key={prompt.id}
prompt={prompt}
values={values}

View File

@@ -1,25 +1,50 @@
import { ChallengePrompt, ChallengeValidation, ProtectionSessionDurations } from '@standardnotes/snjs'
import {
ChallengePrompt,
ChallengeValidation,
MobileDeviceInterface,
ProtectionSessionDurations,
} from '@standardnotes/snjs'
import { FunctionComponent, useEffect, useRef } from 'react'
import DecoratedInput from '@/Components/Input/DecoratedInput'
import DecoratedPasswordInput from '@/Components/Input/DecoratedPasswordInput'
import { ChallengeModalValues } from './ChallengeModalValues'
import Button from '../Button/Button'
import { WebApplication } from '@/Application/Application'
import { InputValue } from './InputValue'
type Props = {
application: WebApplication
prompt: ChallengePrompt
values: ChallengeModalValues
index: number
onValueChange: (value: string | number, prompt: ChallengePrompt) => void
onValueChange: (value: InputValue['value'], prompt: ChallengePrompt) => void
isInvalid: boolean
}
const ChallengeModalPrompt: FunctionComponent<Props> = ({ prompt, values, index, onValueChange, isInvalid }) => {
const ChallengeModalPrompt: FunctionComponent<Props> = ({
application,
prompt,
values,
index,
onValueChange,
isInvalid,
}) => {
const inputRef = useRef<HTMLInputElement>(null)
const biometricsButtonRef = useRef<HTMLButtonElement>(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<Props> = ({ prompt, values, index,
})}
</div>
</div>
) : prompt.validation === ChallengeValidation.Biometric ? (
<div className="min-w-76">
<Button
primary
fullWidth
onClick={async () => {
const authenticated = await (
application.deviceInterface as MobileDeviceInterface
).authenticateWithBiometrics()
onValueChange(authenticated, prompt)
}}
ref={biometricsButtonRef}
>
Tap to use biometrics
</Button>
</div>
) : prompt.secureTextEntry ? (
<DecoratedPasswordInput
ref={inputRef}