diff --git a/packages/mobile/src/Lib/Application.ts b/packages/mobile/src/Lib/Application.ts index 95214e30b..3776b5d29 100644 --- a/packages/mobile/src/Lib/Application.ts +++ b/packages/mobile/src/Lib/Application.ts @@ -9,6 +9,7 @@ import { Environment, IconsController, ItemGroupController, + MobileUnlockTiming, platformFromString, SNApplication, SNComponentManager, @@ -17,7 +18,7 @@ import { Platform } from 'react-native' import { version } from '../../package.json' import { MobileAlertService } from './AlertService' -import { ApplicationState, UnlockTiming } from './ApplicationState' +import { ApplicationState } from './ApplicationState' import { BackupsService } from './BackupsService' import { ComponentManager } from './ComponentManager' import { FilesService } from './FilesService' @@ -122,7 +123,7 @@ export class MobileApplication extends SNApplication { const previouslyLaunched = MobileApplication.getPreviouslyLaunched() const biometricsTiming = this.getAppState().biometricsTiming - if (previouslyLaunched && biometricsTiming === UnlockTiming.OnQuit) { + if (previouslyLaunched && biometricsTiming === MobileUnlockTiming.OnQuit) { const filteredPrompts = challenge.prompts.filter( (prompt: ChallengePrompt) => prompt.validation !== ChallengeValidation.Biometric, ) diff --git a/packages/mobile/src/Lib/ApplicationState.ts b/packages/mobile/src/Lib/ApplicationState.ts index dd10b59c9..22aeca21f 100644 --- a/packages/mobile/src/Lib/ApplicationState.ts +++ b/packages/mobile/src/Lib/ApplicationState.ts @@ -1,3 +1,4 @@ +import { MobileDeviceInterface } from '@Lib/Interface' import { ApplicationEvent, ApplicationService, @@ -8,6 +9,7 @@ import { ContentType, InternalEventBus, isNullOrUndefined, + MobileUnlockTiming, NoteViewController, PayloadEmitSource, PrefKey, @@ -30,14 +32,13 @@ import { KeyboardEventListener, NativeEventSubscription, NativeModules, - Platform, } from 'react-native' -import FlagSecure from 'react-native-flag-secure-android' import { hide, show } from 'react-native-privacy-snapshot' import VersionInfo from 'react-native-version-info' import pjson from '../../package.json' import { MobileApplication } from './Application' import { associateComponentWithNote } from './ComponentManager' + const { PlatformConstants } = NativeModules export enum AppStateType { @@ -66,11 +67,6 @@ export type TabletModeChangeData = { old_isInTabletMode: boolean } -export enum UnlockTiming { - Immediately = 'immediately', - OnQuit = 'on-quit', -} - export enum PasscodeKeyboardType { Default = 'default', Numeric = 'numeric', @@ -103,8 +99,8 @@ export class ApplicationState extends ApplicationService { authenticationInProgress = false multiEditorEnabled = false screenshotPrivacyEnabled?: boolean - passcodeTiming?: UnlockTiming - biometricsTiming?: UnlockTiming + passcodeTiming?: MobileUnlockTiming + biometricsTiming?: MobileUnlockTiming removeHandleReactNativeAppStateChangeListener: NativeEventSubscription removeItemChangesListener?: () => void removePreferencesLoadedListener?: () => void @@ -173,7 +169,9 @@ export class ApplicationState extends ApplicationService { override async onAppLaunch() { MobileApplication.setPreviouslyLaunched() this.screenshotPrivacyEnabled = (await this.getScreenshotPrivacyEnabled()) ?? true - void this.setAndroidScreenshotPrivacy(this.screenshotPrivacyEnabled) + await (this.application.deviceInterface as MobileDeviceInterface).setAndroidScreenshotPrivacy( + this.screenshotPrivacyEnabled, + ) } /** @@ -248,12 +246,6 @@ export class ApplicationState extends ApplicationService { this.biometricsTiming = await this.getBiometricsTiming() } - public async setAndroidScreenshotPrivacy(enable: boolean) { - if (Platform.OS === 'android') { - enable ? FlagSecure.activate() : FlagSecure.deactivate() - } - } - /** * Creates a new editor if one doesn't exist. If one does, we'll replace the * editor's note with an empty one. @@ -486,44 +478,14 @@ export class ApplicationState extends ApplicationService { } } - getPasscodeTimingOptions() { - return [ - { - title: 'Immediately', - key: UnlockTiming.Immediately, - selected: this.passcodeTiming === UnlockTiming.Immediately, - }, - { - title: 'On Quit', - key: UnlockTiming.OnQuit, - selected: this.passcodeTiming === UnlockTiming.OnQuit, - }, - ] - } - - getBiometricsTimingOptions() { - return [ - { - title: 'Immediately', - key: UnlockTiming.Immediately, - selected: this.biometricsTiming === UnlockTiming.Immediately, - }, - { - title: 'On Quit', - key: UnlockTiming.OnQuit, - selected: this.biometricsTiming === UnlockTiming.OnQuit, - }, - ] - } - private async checkAndLockApplication() { const isLocked = await this.application.isLocked() if (!isLocked) { const hasBiometrics = await this.application.hasBiometrics() const hasPasscode = this.application.hasPasscode() - if (hasPasscode && this.passcodeTiming === UnlockTiming.Immediately) { + if (hasPasscode && this.passcodeTiming === MobileUnlockTiming.Immediately) { await this.application.lock() - } else if (hasBiometrics && this.biometricsTiming === UnlockTiming.Immediately && !this.locked) { + } else if (hasBiometrics && this.biometricsTiming === MobileUnlockTiming.Immediately && !this.locked) { const challenge = new Challenge( [new ChallengePrompt(ChallengeValidation.Biometric)], ChallengeReason.ApplicationUnlock, @@ -604,35 +566,33 @@ export class ApplicationState extends ApplicationService { } private async getScreenshotPrivacyEnabled(): Promise { - return this.application.getValue(StorageKey.MobileScreenshotPrivacyEnabled, StorageValueModes.Default) as Promise< - boolean | undefined - > + return this.application.getMobileScreenshotPrivacyEnabled() } - private async getPasscodeTiming(): Promise { + private async getPasscodeTiming(): Promise { return this.application.getValue(StorageKey.MobilePasscodeTiming, StorageValueModes.Nonwrapped) as Promise< - UnlockTiming | undefined + MobileUnlockTiming | undefined > } - private async getBiometricsTiming(): Promise { + private async getBiometricsTiming(): Promise { return this.application.getValue(StorageKey.MobileBiometricsTiming, StorageValueModes.Nonwrapped) as Promise< - UnlockTiming | undefined + MobileUnlockTiming | undefined > } public async setScreenshotPrivacyEnabled(enabled: boolean) { - await this.application.setValue(StorageKey.MobileScreenshotPrivacyEnabled, enabled, StorageValueModes.Default) + await this.application.setMobileScreenshotPrivacyEnabled(enabled) this.screenshotPrivacyEnabled = enabled - void this.setAndroidScreenshotPrivacy(enabled) + await (this.application.deviceInterface as MobileDeviceInterface).setAndroidScreenshotPrivacy(enabled) } - public async setPasscodeTiming(timing: UnlockTiming) { + public async setPasscodeTiming(timing: MobileUnlockTiming) { await this.application.setValue(StorageKey.MobilePasscodeTiming, timing, StorageValueModes.Nonwrapped) this.passcodeTiming = timing } - public async setBiometricsTiming(timing: UnlockTiming) { + public async setBiometricsTiming(timing: MobileUnlockTiming) { await this.application.setValue(StorageKey.MobileBiometricsTiming, timing, StorageValueModes.Nonwrapped) this.biometricsTiming = timing } diff --git a/packages/mobile/src/Lib/Interface.ts b/packages/mobile/src/Lib/Interface.ts index d0c1b4f79..f3a02ac41 100644 --- a/packages/mobile/src/Lib/Interface.ts +++ b/packages/mobile/src/Lib/Interface.ts @@ -12,6 +12,7 @@ import { } from '@standardnotes/snjs' import { Alert, Linking, Platform } from 'react-native' import FingerprintScanner from 'react-native-fingerprint-scanner' +import FlagSecure from 'react-native-flag-secure-android' import Keychain from './Keychain' export type BiometricsType = 'Fingerprint' | 'Face ID' | 'Biometrics' | 'Touch ID' @@ -295,6 +296,12 @@ export class MobileDeviceInterface implements DeviceInterface { await Keychain.clearKeys() } + async setAndroidScreenshotPrivacy(enable: boolean): Promise { + if (Platform.OS === 'android') { + enable ? FlagSecure.activate() : FlagSecure.deactivate() + } + } + openUrl(url: string) { const showAlert = () => { Alert.alert('Unable to Open', `Unable to open URL ${url}.`) diff --git a/packages/mobile/src/Screens/Authenticate/Authenticate.tsx b/packages/mobile/src/Screens/Authenticate/Authenticate.tsx index e77f6db6a..c9e13065a 100644 --- a/packages/mobile/src/Screens/Authenticate/Authenticate.tsx +++ b/packages/mobile/src/Screens/Authenticate/Authenticate.tsx @@ -197,7 +197,7 @@ export const Authenticate = ({ state: AuthenticationValueStateType.Pending, }) - if (application?.getAppState().screenshotPrivacyEnabled) { + if (await application?.getMobileScreenshotPrivacyEnabled()) { hide() } diff --git a/packages/mobile/src/Screens/InputModal/PasscodeInputModal.tsx b/packages/mobile/src/Screens/InputModal/PasscodeInputModal.tsx index 7708b5682..27eeac12c 100644 --- a/packages/mobile/src/Screens/InputModal/PasscodeInputModal.tsx +++ b/packages/mobile/src/Screens/InputModal/PasscodeInputModal.tsx @@ -1,4 +1,4 @@ -import { PasscodeKeyboardType, UnlockTiming } from '@Lib/ApplicationState' +import { PasscodeKeyboardType } from '@Lib/ApplicationState' import { ApplicationContext } from '@Root/ApplicationContext' import { ButtonCell } from '@Root/Components/ButtonCell' import { Option, SectionedOptionsTableCell } from '@Root/Components/SectionedOptionsTableCell' @@ -6,6 +6,7 @@ import { SectionedTableCell } from '@Root/Components/SectionedTableCell' import { TableSection } from '@Root/Components/TableSection' import { ModalStackNavigationProp } from '@Root/ModalStack' import { SCREEN_INPUT_MODAL_PASSCODE } from '@Root/Screens/screens' +import { MobileUnlockTiming } from '@standardnotes/snjs' import { ThemeServiceContext } from '@Style/ThemeService' import React, { useContext, useMemo, useRef, useState } from 'react' import { Keyboard, KeyboardType, Platform, TextInput } from 'react-native' @@ -50,7 +51,7 @@ export const PasscodeInputModal = (props: Props) => { } else { await application?.addPasscode(text) await application?.getAppState().setPasscodeKeyboardType(keyboardType as PasscodeKeyboardType) - await application?.getAppState().setPasscodeTiming(UnlockTiming.OnQuit) + await application?.getAppState().setPasscodeTiming(MobileUnlockTiming.OnQuit) setSettingPassocode(false) props.navigation.goBack() } diff --git a/packages/mobile/src/Screens/Settings/Sections/SecuritySection.tsx b/packages/mobile/src/Screens/Settings/Sections/SecuritySection.tsx index 772d3103e..74d41a4d7 100644 --- a/packages/mobile/src/Screens/Settings/Sections/SecuritySection.tsx +++ b/packages/mobile/src/Screens/Settings/Sections/SecuritySection.tsx @@ -1,4 +1,3 @@ -import { UnlockTiming } from '@Lib/ApplicationState' import { MobileDeviceInterface } from '@Lib/Interface' import { useFocusEffect, useNavigation } from '@react-navigation/native' import { ApplicationContext } from '@Root/ApplicationContext' @@ -8,7 +7,7 @@ import { SectionHeader } from '@Root/Components/SectionHeader' import { TableSection } from '@Root/Components/TableSection' import { ModalStackNavigationProp } from '@Root/ModalStack' import { SCREEN_INPUT_MODAL_PASSCODE, SCREEN_SETTINGS } from '@Root/Screens/screens' -import { StorageEncryptionPolicy } from '@standardnotes/snjs' +import { MobileUnlockTiming, StorageEncryptionPolicy } from '@standardnotes/snjs' import React, { useCallback, useContext, useEffect, useState } from 'react' import { Platform } from 'react-native' import { Title } from './SecuritySection.styled' @@ -32,16 +31,14 @@ export const SecuritySection = (props: Props) => { const [hasBiometrics, setHasBiometrics] = useState(false) const [supportsBiometrics, setSupportsBiometrics] = useState(false) const [biometricsTimingOptions, setBiometricsTimingOptions] = useState(() => - application!.getAppState().getBiometricsTimingOptions(), - ) - const [passcodeTimingOptions, setPasscodeTimingOptions] = useState(() => - application!.getAppState().getPasscodeTimingOptions(), + application!.getBiometricsTimingOptions(), ) + const [passcodeTimingOptions, setPasscodeTimingOptions] = useState(() => application!.getPasscodeTimingOptions()) useEffect(() => { let mounted = true const getHasScreenshotPrivacy = async () => { - const hasScreenshotPrivacyEnabled = await application?.getAppState().screenshotPrivacyEnabled + const hasScreenshotPrivacyEnabled = (await application?.getMobileScreenshotPrivacyEnabled()) ?? true if (mounted) { setHasScreenshotPrivacy(hasScreenshotPrivacyEnabled) } @@ -71,7 +68,7 @@ export const SecuritySection = (props: Props) => { useFocusEffect( useCallback(() => { if (props.hasPasscode) { - setPasscodeTimingOptions(() => application!.getAppState().getPasscodeTimingOptions()) + setPasscodeTimingOptions(() => application!.getPasscodeTimingOptions()) } }, [application, props.hasPasscode]), ) @@ -127,14 +124,14 @@ export const SecuritySection = (props: Props) => { const biometricTitle = hasBiometrics ? 'Disable Biometrics Lock' : 'Enable Biometrics Lock' - const setBiometricsTiming = async (timing: UnlockTiming) => { + const setBiometricsTiming = async (timing: MobileUnlockTiming) => { await application?.getAppState().setBiometricsTiming(timing) - setBiometricsTimingOptions(() => application!.getAppState().getBiometricsTimingOptions()) + setBiometricsTimingOptions(() => application!.getBiometricsTimingOptions()) } - const setPasscodeTiming = async (timing: UnlockTiming) => { + const setPasscodeTiming = async (timing: MobileUnlockTiming) => { await application?.getAppState().setPasscodeTiming(timing) - setPasscodeTimingOptions(() => application!.getAppState().getPasscodeTimingOptions()) + setPasscodeTimingOptions(() => application!.getPasscodeTimingOptions()) } const onScreenshotPrivacyPress = async () => { @@ -157,7 +154,7 @@ export const SecuritySection = (props: Props) => { } else { setHasBiometrics(true) await application?.enableBiometrics() - await setBiometricsTiming(UnlockTiming.OnQuit) + await setBiometricsTiming(MobileUnlockTiming.OnQuit) props.updateProtectionsAvailable() } } @@ -231,7 +228,7 @@ export const SecuritySection = (props: Props) => { leftAligned title={'Require Passcode'} options={passcodeTimingOptions} - onPress={(option: Option) => setPasscodeTiming(option.key as UnlockTiming)} + onPress={(option: Option) => setPasscodeTiming(option.key as MobileUnlockTiming)} /> )} @@ -240,7 +237,7 @@ export const SecuritySection = (props: Props) => { leftAligned title={'Require Biometrics'} options={biometricsTimingOptions} - onPress={(option: Option) => setBiometricsTiming(option.key as UnlockTiming)} + onPress={(option: Option) => setBiometricsTiming(option.key as MobileUnlockTiming)} /> )} diff --git a/packages/services/src/Domain/Device/MobileDeviceInterface.ts b/packages/services/src/Domain/Device/MobileDeviceInterface.ts index e69de05f4..68d2d9061 100644 --- a/packages/services/src/Domain/Device/MobileDeviceInterface.ts +++ b/packages/services/src/Domain/Device/MobileDeviceInterface.ts @@ -5,4 +5,8 @@ export interface MobileDeviceInterface extends DeviceInterface { environment: Environment.Mobile getRawKeychainValue(): Promise + getDeviceBiometricsAvailability(): Promise + setAndroidScreenshotPrivacy(enable: boolean): Promise + getMobileScreenshotPrivacyEnabled(): Promise + setMobileScreenshotPrivacyEnabled(isEnabled: boolean): Promise } diff --git a/packages/snjs/lib/Application/Application.ts b/packages/snjs/lib/Application/Application.ts index ba249866b..27464192e 100644 --- a/packages/snjs/lib/Application/Application.ts +++ b/packages/snjs/lib/Application/Application.ts @@ -927,6 +927,34 @@ export class SNApplication return this.deinit(this.getDeinitMode(), DeinitSource.Lock) } + async setBiometricsTiming(timing: InternalServices.MobileUnlockTiming) { + return this.protectionService.setBiometricsTiming(timing) + } + + async getMobileScreenshotPrivacyEnabled(): Promise { + return this.protectionService.getMobileScreenshotPrivacyEnabled() + } + + async setMobileScreenshotPrivacyEnabled(isEnabled: boolean) { + return this.protectionService.setMobileScreenshotPrivacyEnabled(isEnabled) + } + + async loadMobileUnlockTiming() { + return this.protectionService.loadMobileUnlockTiming() + } + + getBiometricsTimingOptions() { + return this.protectionService.getBiometricsTimingOptions() + } + + getPasscodeTimingOptions() { + return this.protectionService.getPasscodeTimingOptions() + } + + isNativeMobileWeb() { + return this.environment === Environment.NativeMobileWeb + } + getDeinitMode(): DeinitMode { const value = this.getValue(StorageKey.DeinitMode) if (value === 'hard') { diff --git a/packages/snjs/lib/Services/Protection/ProtectionService.ts b/packages/snjs/lib/Services/Protection/ProtectionService.ts index 17fb3ee6d..840524c95 100644 --- a/packages/snjs/lib/Services/Protection/ProtectionService.ts +++ b/packages/snjs/lib/Services/Protection/ProtectionService.ts @@ -24,6 +24,11 @@ export enum ProtectionEvent { UnprotectedSessionExpired = 'UnprotectedSessionExpired', } +export enum MobileUnlockTiming { + Immediately = 'immediately', + OnQuit = 'on-quit', +} + export const ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction = 30 export enum UnprotectedAccessSecondsDuration { @@ -63,6 +68,8 @@ export const ProtectionSessionDurations = [ */ export class SNProtectionService extends AbstractService implements ProtectionsClientInterface { private sessionExpiryTimeout = -1 + private mobilePasscodeTiming: MobileUnlockTiming | undefined = MobileUnlockTiming.Immediately + private mobileBiometricsTiming: MobileUnlockTiming | undefined = MobileUnlockTiming.Immediately constructor( private protocolService: EncryptionService, @@ -224,6 +231,68 @@ export class SNProtectionService extends AbstractService implem }) } + getPasscodeTimingOptions() { + return [ + { + title: 'Immediately', + key: MobileUnlockTiming.Immediately, + selected: this.mobilePasscodeTiming === MobileUnlockTiming.Immediately, + }, + { + title: 'On Quit', + key: MobileUnlockTiming.OnQuit, + selected: this.mobilePasscodeTiming === MobileUnlockTiming.OnQuit, + }, + ] + } + + getBiometricsTimingOptions() { + return [ + { + title: 'Immediately', + key: MobileUnlockTiming.Immediately, + selected: this.mobileBiometricsTiming === MobileUnlockTiming.Immediately, + }, + { + title: 'On Quit', + key: MobileUnlockTiming.OnQuit, + selected: this.mobileBiometricsTiming === MobileUnlockTiming.OnQuit, + }, + ] + } + + private async getBiometricsTiming(): Promise { + return this.storageService.getValue>( + StorageKey.MobileBiometricsTiming, + StorageValueModes.Nonwrapped, + ) + } + + private async getPasscodeTiming(): Promise { + return this.storageService.getValue>( + StorageKey.MobilePasscodeTiming, + StorageValueModes.Nonwrapped, + ) + } + + async setBiometricsTiming(timing: MobileUnlockTiming) { + await this.storageService.setValue(StorageKey.MobileBiometricsTiming, timing, StorageValueModes.Nonwrapped) + this.mobileBiometricsTiming = timing + } + + async setMobileScreenshotPrivacyEnabled(isEnabled: boolean) { + return this.storageService.setValue(StorageKey.MobileScreenshotPrivacyEnabled, isEnabled, StorageValueModes.Default) + } + + async getMobileScreenshotPrivacyEnabled(): Promise { + return this.storageService.getValue(StorageKey.MobileScreenshotPrivacyEnabled, StorageValueModes.Default) + } + + async loadMobileUnlockTiming() { + this.mobilePasscodeTiming = await this.getPasscodeTiming() + this.mobileBiometricsTiming = await this.getBiometricsTiming() + } + private async validateOrRenewSession( reason: ChallengeReason, { fallBackToAccountPassword = true, requireAccountPassword = false } = {}, @@ -270,7 +339,9 @@ export class SNProtectionService extends AbstractService implem chosenSessionLength, ), ) + const response = await this.challengeService.promptForChallengeResponse(new Challenge(prompts, reason, true)) + if (response) { const length = response.values.find( (value) => value.prompt.validation === ChallengeValidation.ProtectionSessionDuration, diff --git a/packages/snjs/lib/Services/Storage/DiskStorageService.ts b/packages/snjs/lib/Services/Storage/DiskStorageService.ts index 838a65ada..828680bd7 100644 --- a/packages/snjs/lib/Services/Storage/DiskStorageService.ts +++ b/packages/snjs/lib/Services/Storage/DiskStorageService.ts @@ -90,7 +90,10 @@ export class DiskStorageService extends Services.AbstractService implements Serv } public setEncryptionPolicy(encryptionPolicy: Services.StorageEncryptionPolicy, persist = true): void { - if (encryptionPolicy === Services.StorageEncryptionPolicy.Disabled && this.environment !== Environment.Mobile) { + if ( + encryptionPolicy === Services.StorageEncryptionPolicy.Disabled && + ![Environment.Mobile, Environment.NativeMobileWeb].includes(this.environment) + ) { throw Error('Disabling storage encryption is only available on mobile.') } diff --git a/packages/web/src/javascripts/App.tsx b/packages/web/src/javascripts/App.tsx index dc97e033a..6bcd45722 100644 --- a/packages/web/src/javascripts/App.tsx +++ b/packages/web/src/javascripts/App.tsx @@ -98,6 +98,7 @@ if (IsWebPlatform) { setTimeout(() => { const device = window.reactNativeDevice || new WebDevice(WebAppVersion) + startApplication(window.defaultSyncServer, device, window.enabledUnfinishedFeatures, window.websocketUrl).catch( console.error, ) diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Security/BiometricsLock.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Security/BiometricsLock.tsx new file mode 100644 index 000000000..c9814b227 --- /dev/null +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Security/BiometricsLock.tsx @@ -0,0 +1,97 @@ +import { observer } from 'mobx-react-lite' +import { useCallback, useEffect, useState } from 'react' +import { WebApplication } from '@/Application/Application' +import { MobileDeviceInterface } from '@standardnotes/services' +import { MobileUnlockTiming } from '@standardnotes/snjs' +import PreferencesSegment from '@/Components/Preferences/PreferencesComponents/PreferencesSegment' +import { Title } from '@/Components/Preferences/PreferencesComponents/Content' +import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/PreferencesGroup' +import Button from '@/Components/Button/Button' +import { classNames } from '@/Utils/ConcatenateClassNames' + +type Props = { + application: WebApplication +} + +const BiometricsLock = ({ application }: Props) => { + const [hasBiometrics, setHasBiometrics] = useState(false) + const [supportsBiometrics, setSupportsBiometrics] = useState(false) + const [biometricsTimingOptions, setBiometricsTimingOptions] = useState(() => application.getBiometricsTimingOptions()) + + useEffect(() => { + const getHasBiometrics = async () => { + const appHasBiometrics = await application.hasBiometrics() + setHasBiometrics(appHasBiometrics) + } + + const hasBiometricsSupport = async () => { + const hasBiometricsAvailable = await ( + application.deviceInterface as MobileDeviceInterface + ).getDeviceBiometricsAvailability?.() + setSupportsBiometrics(hasBiometricsAvailable) + } + void getHasBiometrics() + void hasBiometricsSupport() + }, [application]) + + const setBiometricsTimingValue = async (timing: MobileUnlockTiming) => { + await application.setBiometricsTiming(timing) + setBiometricsTimingOptions(() => application.getBiometricsTimingOptions()) + } + + const disableBiometrics = useCallback(async () => { + if (await application.disableBiometrics()) { + setHasBiometrics(false) + } + }, [application]) + + const onBiometricsPress = async () => { + if (hasBiometrics) { + await disableBiometrics() + } else { + setHasBiometrics(true) + await application.enableBiometrics() + await setBiometricsTimingValue(MobileUnlockTiming.OnQuit) + } + } + + const biometricTitle = hasBiometrics ? 'Disable Biometrics Lock' : 'Enable Biometrics Lock' + + if (!supportsBiometrics) { + return null + } + + return ( +
+ + + Biometrics Lock +
+ ) +} + +export default observer(BiometricsLock) diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Security/MultitaskingPrivacy.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Security/MultitaskingPrivacy.tsx new file mode 100644 index 000000000..d5244d635 --- /dev/null +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Security/MultitaskingPrivacy.tsx @@ -0,0 +1,51 @@ +import { observer } from 'mobx-react-lite' +import { WebApplication } from '@/Application/Application' +import { isIOS } from '@/Utils' +import { useEffect, useState } from 'react' +import { MobileDeviceInterface } from '@standardnotes/services' +import PreferencesSegment from '@/Components/Preferences/PreferencesComponents/PreferencesSegment' +import { Title } from '@/Components/Preferences/PreferencesComponents/Content' +import Button from '@/Components/Button/Button' +import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/PreferencesGroup' + +type Props = { + application: WebApplication +} + +const MultitaskingPrivacy = ({ application }: Props) => { + const [hasScreenshotPrivacy, setHasScreenshotPrivacy] = useState(false) + + useEffect(() => { + const getHasScreenshotPrivacy = async () => { + const hasScreenshotPrivacyEnabled = (await application.getMobileScreenshotPrivacyEnabled()) ?? true + setHasScreenshotPrivacy(hasScreenshotPrivacyEnabled) + } + void getHasScreenshotPrivacy() + }, [application]) + + const onScreenshotPrivacyPress = async () => { + const enable = !hasScreenshotPrivacy + setHasScreenshotPrivacy(enable) + + await application.setMobileScreenshotPrivacyEnabled(enable) + await (application.deviceInterface as MobileDeviceInterface).setAndroidScreenshotPrivacy(enable) + } + + const screenshotPrivacyFeatureText = isIOS() ? 'Multitasking Privacy' : 'Multitasking/Screenshot Privacy' + const screenshotPrivacyTitle = hasScreenshotPrivacy + ? `Disable ${screenshotPrivacyFeatureText}` + : `Enable ${screenshotPrivacyFeatureText}` + + return ( +
+ + + {screenshotPrivacyFeatureText} +
+ ) +} + +export default observer(MultitaskingPrivacy) diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Security/Security.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Security/Security.tsx index ca37da458..4ab7389aa 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Security/Security.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Security/Security.tsx @@ -9,23 +9,34 @@ import Privacy from './Privacy' import Protections from './Protections' import ErroredItems from './ErroredItems' import PreferencesPane from '@/Components/Preferences/PreferencesComponents/PreferencesPane' +import BiometricsLock from '@/Components/Preferences/Panes/Security/BiometricsLock' +import MultitaskingPrivacy from '@/Components/Preferences/Panes/Security/MultitaskingPrivacy' interface SecurityProps extends MfaProps { viewControllerManager: ViewControllerManager application: WebApplication } -const Security: FunctionComponent = (props) => ( - - - {props.application.items.invalidItems.length > 0 && ( - - )} - - - - {props.application.getUser() && } - -) +const SHOW_MULTITASKING_PRIVACY = false +const SHOW_BIOMETRICS_LOCK = false + +const Security: FunctionComponent = (props) => { + const isNativeMobileWeb = props.application.isNativeMobileWeb() + + return ( + + + {props.application.items.invalidItems.length > 0 && ( + + )} + + + {SHOW_MULTITASKING_PRIVACY && isNativeMobileWeb && } + + {SHOW_BIOMETRICS_LOCK && isNativeMobileWeb && } + {props.application.getUser() && } + + ) +} export default Security