diff --git a/packages/mobile/src/Lib/Interface.ts b/packages/mobile/src/Lib/Interface.ts index 7dd5d49ba..b89a24e0b 100644 --- a/packages/mobile/src/Lib/Interface.ts +++ b/packages/mobile/src/Lib/Interface.ts @@ -13,7 +13,7 @@ import { TransferPayload, UuidString, } 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 FingerprintScanner from 'react-native-fingerprint-scanner' import FlagSecure from 'react-native-flag-secure-android' @@ -610,4 +610,8 @@ export class MobileDevice implements MobileDeviceInterface { isUrlComponentUrl(url: string): boolean { return Array.from(this.componentUrls.values()).includes(url) } + + async getAppState(): Promise { + return AppState.currentState + } } diff --git a/packages/services/src/Domain/Device/MobileDeviceInterface.ts b/packages/services/src/Domain/Device/MobileDeviceInterface.ts index aa228b6ac..21da72e7e 100644 --- a/packages/services/src/Domain/Device/MobileDeviceInterface.ts +++ b/packages/services/src/Domain/Device/MobileDeviceInterface.ts @@ -20,4 +20,5 @@ export interface MobileDeviceInterface extends DeviceInterface { addComponentUrl(componentUuid: string, componentUrl: string): void removeComponentUrl(componentUuid: string): void isUrlComponentUrl(url: string): boolean + getAppState(): Promise<'active' | 'background' | 'inactive' | 'unknown' | 'extension'> } diff --git a/packages/web/src/javascripts/Application/Application.ts b/packages/web/src/javascripts/Application/Application.ts index d2da77961..6efbf0490 100644 --- a/packages/web/src/javascripts/Application/Application.ts +++ b/packages/web/src/javascripts/Application/Application.ts @@ -35,7 +35,7 @@ import { ThemeManager, WebAlertService, } from '@standardnotes/ui-services' -import { MobileWebReceiver } from '../NativeMobileWeb/MobileWebReceiver' +import { MobileWebReceiver, NativeMobileEventListener } from '../NativeMobileWeb/MobileWebReceiver' import { AndroidBackHandler } from '@/NativeMobileWeb/AndroidBackHandler' import { PrefDefaults } from '@/Constants/PrefDefaults' import { setViewportHeightWithFallback } from '@/setViewportHeightWithFallback' @@ -330,4 +330,12 @@ export class WebApplication extends SNApplication implements WebApplicationInter openPurchaseFlow(): void { this.getViewControllerManager().purchaseFlowController.openPurchaseFlow() } + + addNativeMobileEventListener = (listener: NativeMobileEventListener) => { + if (!this.mobileWebReceiver) { + return + } + + return this.mobileWebReceiver.addReactListener(listener) + } } diff --git a/packages/web/src/javascripts/Components/ChallengeModal/ChallengePrompt.tsx b/packages/web/src/javascripts/Components/ChallengeModal/ChallengePrompt.tsx index 0e9c09e0f..34b8d05b3 100644 --- a/packages/web/src/javascripts/Components/ChallengeModal/ChallengePrompt.tsx +++ b/packages/web/src/javascripts/Components/ChallengeModal/ChallengePrompt.tsx @@ -1,5 +1,10 @@ -import { ChallengePrompt, ChallengeValidation, ProtectionSessionDurations } from '@standardnotes/snjs' -import { FunctionComponent, useEffect, useRef } from 'react' +import { + ChallengePrompt, + ChallengeValidation, + ProtectionSessionDurations, + ReactNativeToWebEvent, +} from '@standardnotes/snjs' +import { FunctionComponent, useCallback, useEffect, useRef } from 'react' import DecoratedInput from '@/Components/Input/DecoratedInput' import DecoratedPasswordInput from '@/Components/Input/DecoratedPasswordInput' import { ChallengeModalValues } from './ChallengeModalValues' @@ -27,6 +32,40 @@ const ChallengeModalPrompt: FunctionComponent = ({ const inputRef = useRef(null) const biometricsButtonRef = useRef(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(() => { const isNotFirstPrompt = index !== 0 @@ -34,12 +73,8 @@ const ChallengeModalPrompt: FunctionComponent = ({ return } - if (prompt.validation === ChallengeValidation.Biometric) { - biometricsButtonRef.current?.click() - } else { - inputRef.current?.focus() - } - }, [index, prompt.validation]) + void activatePrompt() + }, [activatePrompt, index]) useEffect(() => { if (isInvalid) { diff --git a/packages/web/src/javascripts/NativeMobileWeb/MobileWebReceiver.ts b/packages/web/src/javascripts/NativeMobileWeb/MobileWebReceiver.ts index 15a14921c..434e61569 100644 --- a/packages/web/src/javascripts/NativeMobileWeb/MobileWebReceiver.ts +++ b/packages/web/src/javascripts/NativeMobileWeb/MobileWebReceiver.ts @@ -1,6 +1,10 @@ import { ReactNativeToWebEvent, WebApplicationInterface } from '@standardnotes/snjs' +export type NativeMobileEventListener = (event: ReactNativeToWebEvent) => void + export class MobileWebReceiver { + private listeners: Set = new Set() + constructor(private application: WebApplicationInterface) { this.listenForNativeMobileEvents() } @@ -39,6 +43,14 @@ export class MobileWebReceiver { } } + addReactListener = (listener: NativeMobileEventListener) => { + this.listeners.add(listener) + + return () => { + this.listeners.delete(listener) + } + } + handleNativeEvent(event: ReactNativeToWebEvent) { switch (event) { case ReactNativeToWebEvent.EnteringBackground: @@ -60,5 +72,7 @@ export class MobileWebReceiver { default: break } + + this.listeners.forEach((listener) => listener(event)) } }