feat: auto-activate biometrics prompt when mobile regains focus (#1891)
This commit is contained in:
@@ -13,7 +13,7 @@ import {
|
|||||||
TransferPayload,
|
TransferPayload,
|
||||||
UuidString,
|
UuidString,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import { Alert, Linking, PermissionsAndroid, Platform, StatusBar } from 'react-native'
|
import { Alert, AppState, AppStateStatus, Linking, PermissionsAndroid, Platform, StatusBar } from 'react-native'
|
||||||
import FileViewer from 'react-native-file-viewer'
|
import FileViewer from 'react-native-file-viewer'
|
||||||
import FingerprintScanner from 'react-native-fingerprint-scanner'
|
import FingerprintScanner from 'react-native-fingerprint-scanner'
|
||||||
import FlagSecure from 'react-native-flag-secure-android'
|
import FlagSecure from 'react-native-flag-secure-android'
|
||||||
@@ -610,4 +610,8 @@ export class MobileDevice implements MobileDeviceInterface {
|
|||||||
isUrlComponentUrl(url: string): boolean {
|
isUrlComponentUrl(url: string): boolean {
|
||||||
return Array.from(this.componentUrls.values()).includes(url)
|
return Array.from(this.componentUrls.values()).includes(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAppState(): Promise<AppStateStatus> {
|
||||||
|
return AppState.currentState
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,4 +20,5 @@ export interface MobileDeviceInterface extends DeviceInterface {
|
|||||||
addComponentUrl(componentUuid: string, componentUrl: string): void
|
addComponentUrl(componentUuid: string, componentUrl: string): void
|
||||||
removeComponentUrl(componentUuid: string): void
|
removeComponentUrl(componentUuid: string): void
|
||||||
isUrlComponentUrl(url: string): boolean
|
isUrlComponentUrl(url: string): boolean
|
||||||
|
getAppState(): Promise<'active' | 'background' | 'inactive' | 'unknown' | 'extension'>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import {
|
|||||||
ThemeManager,
|
ThemeManager,
|
||||||
WebAlertService,
|
WebAlertService,
|
||||||
} from '@standardnotes/ui-services'
|
} from '@standardnotes/ui-services'
|
||||||
import { MobileWebReceiver } from '../NativeMobileWeb/MobileWebReceiver'
|
import { MobileWebReceiver, NativeMobileEventListener } from '../NativeMobileWeb/MobileWebReceiver'
|
||||||
import { AndroidBackHandler } from '@/NativeMobileWeb/AndroidBackHandler'
|
import { AndroidBackHandler } from '@/NativeMobileWeb/AndroidBackHandler'
|
||||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||||
import { setViewportHeightWithFallback } from '@/setViewportHeightWithFallback'
|
import { setViewportHeightWithFallback } from '@/setViewportHeightWithFallback'
|
||||||
@@ -330,4 +330,12 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
|||||||
openPurchaseFlow(): void {
|
openPurchaseFlow(): void {
|
||||||
this.getViewControllerManager().purchaseFlowController.openPurchaseFlow()
|
this.getViewControllerManager().purchaseFlowController.openPurchaseFlow()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addNativeMobileEventListener = (listener: NativeMobileEventListener) => {
|
||||||
|
if (!this.mobileWebReceiver) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.mobileWebReceiver.addReactListener(listener)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { ChallengePrompt, ChallengeValidation, ProtectionSessionDurations } from '@standardnotes/snjs'
|
import {
|
||||||
import { FunctionComponent, useEffect, useRef } from 'react'
|
ChallengePrompt,
|
||||||
|
ChallengeValidation,
|
||||||
|
ProtectionSessionDurations,
|
||||||
|
ReactNativeToWebEvent,
|
||||||
|
} from '@standardnotes/snjs'
|
||||||
|
import { FunctionComponent, useCallback, useEffect, useRef } from 'react'
|
||||||
import DecoratedInput from '@/Components/Input/DecoratedInput'
|
import DecoratedInput from '@/Components/Input/DecoratedInput'
|
||||||
import DecoratedPasswordInput from '@/Components/Input/DecoratedPasswordInput'
|
import DecoratedPasswordInput from '@/Components/Input/DecoratedPasswordInput'
|
||||||
import { ChallengeModalValues } from './ChallengeModalValues'
|
import { ChallengeModalValues } from './ChallengeModalValues'
|
||||||
@@ -27,6 +32,40 @@ const ChallengeModalPrompt: FunctionComponent<Props> = ({
|
|||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
const biometricsButtonRef = useRef<HTMLButtonElement>(null)
|
const biometricsButtonRef = useRef<HTMLButtonElement>(null)
|
||||||
|
|
||||||
|
const activatePrompt = useCallback(async () => {
|
||||||
|
if (prompt.validation === ChallengeValidation.Biometric) {
|
||||||
|
if (application.isNativeMobileWeb()) {
|
||||||
|
const appState = await application.mobileDevice().getAppState()
|
||||||
|
|
||||||
|
if (appState !== 'active') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
biometricsButtonRef.current?.click()
|
||||||
|
} else {
|
||||||
|
inputRef.current?.focus()
|
||||||
|
}
|
||||||
|
}, [application, prompt.validation])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!application.isNativeMobileWeb()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const disposeListener = application.addNativeMobileEventListener((event: ReactNativeToWebEvent) => {
|
||||||
|
if (event === ReactNativeToWebEvent.GainingFocus) {
|
||||||
|
void activatePrompt()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (disposeListener) {
|
||||||
|
disposeListener()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [activatePrompt, application])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isNotFirstPrompt = index !== 0
|
const isNotFirstPrompt = index !== 0
|
||||||
|
|
||||||
@@ -34,12 +73,8 @@ const ChallengeModalPrompt: FunctionComponent<Props> = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prompt.validation === ChallengeValidation.Biometric) {
|
void activatePrompt()
|
||||||
biometricsButtonRef.current?.click()
|
}, [activatePrompt, index])
|
||||||
} else {
|
|
||||||
inputRef.current?.focus()
|
|
||||||
}
|
|
||||||
}, [index, prompt.validation])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isInvalid) {
|
if (isInvalid) {
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { ReactNativeToWebEvent, WebApplicationInterface } from '@standardnotes/snjs'
|
import { ReactNativeToWebEvent, WebApplicationInterface } from '@standardnotes/snjs'
|
||||||
|
|
||||||
|
export type NativeMobileEventListener = (event: ReactNativeToWebEvent) => void
|
||||||
|
|
||||||
export class MobileWebReceiver {
|
export class MobileWebReceiver {
|
||||||
|
private listeners: Set<NativeMobileEventListener> = new Set()
|
||||||
|
|
||||||
constructor(private application: WebApplicationInterface) {
|
constructor(private application: WebApplicationInterface) {
|
||||||
this.listenForNativeMobileEvents()
|
this.listenForNativeMobileEvents()
|
||||||
}
|
}
|
||||||
@@ -39,6 +43,14 @@ export class MobileWebReceiver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addReactListener = (listener: NativeMobileEventListener) => {
|
||||||
|
this.listeners.add(listener)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.listeners.delete(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleNativeEvent(event: ReactNativeToWebEvent) {
|
handleNativeEvent(event: ReactNativeToWebEvent) {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case ReactNativeToWebEvent.EnteringBackground:
|
case ReactNativeToWebEvent.EnteringBackground:
|
||||||
@@ -60,5 +72,7 @@ export class MobileWebReceiver {
|
|||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.listeners.forEach((listener) => listener(event))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user