feat: mobile security prefs (#1496)

* feat: move mobile-specific security items to Web when rendered in WebView

* feat: better UI for biometrics section

* feat: move Multitasking Privacy section to WebView (mostly UI)

* feat: move Multitasking Privacy section to WebView (going to understand why in WebView multitasking privacy value is auto-changed after reopening the WebView)

* feat: store MultitaskingPrivacy value as "NonWrapped" so that it's the same both on mobile and WebView

* feat: open WebView correctly when "Storage Encryption" is disabled on mobile

* fix: remove unnecessary changes and comments

* chore: revert ios-related unneeded changes

* fix: let Android to correctly recognize the NativeMobileWeb environment when opening WebView on Android

* fix: correct styles for the selected state of Biometrics/Passcode options

* chore: code cleanup

* fix: store Multitasking/Screenshot Privacy in the `Default` storage value mode

* chore: remove comment

* fix: use application's method instead of directly updating Screenshot Privacy preference

* fix: remove unused variable

* fix: use methods from Application and MobileDeviceInterface in all places, remove duplicate code

* fix: hide Multitasking Privacy and Biometrics Lock in WebView

Co-authored-by: Aman Harwara
This commit is contained in:
Vardan Hakobyan
2022-09-14 13:07:10 +04:00
committed by GitHub
parent e73d187b65
commit d7aca2c13a
14 changed files with 324 additions and 92 deletions

View File

@@ -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,
)

View File

@@ -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<boolean | undefined> {
return this.application.getValue(StorageKey.MobileScreenshotPrivacyEnabled, StorageValueModes.Default) as Promise<
boolean | undefined
>
return this.application.getMobileScreenshotPrivacyEnabled()
}
private async getPasscodeTiming(): Promise<UnlockTiming | undefined> {
private async getPasscodeTiming(): Promise<MobileUnlockTiming | undefined> {
return this.application.getValue(StorageKey.MobilePasscodeTiming, StorageValueModes.Nonwrapped) as Promise<
UnlockTiming | undefined
MobileUnlockTiming | undefined
>
}
private async getBiometricsTiming(): Promise<UnlockTiming | undefined> {
private async getBiometricsTiming(): Promise<MobileUnlockTiming | undefined> {
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
}

View File

@@ -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<void> {
if (Platform.OS === 'android') {
enable ? FlagSecure.activate() : FlagSecure.deactivate()
}
}
openUrl(url: string) {
const showAlert = () => {
Alert.alert('Unable to Open', `Unable to open URL ${url}.`)

View File

@@ -197,7 +197,7 @@ export const Authenticate = ({
state: AuthenticationValueStateType.Pending,
})
if (application?.getAppState().screenshotPrivacyEnabled) {
if (await application?.getMobileScreenshotPrivacyEnabled()) {
hide()
}

View File

@@ -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()
}

View File

@@ -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)}
/>
)}
</TableSection>

View File

@@ -5,4 +5,8 @@ export interface MobileDeviceInterface extends DeviceInterface {
environment: Environment.Mobile
getRawKeychainValue(): Promise<RawKeychainValue | undefined>
getDeviceBiometricsAvailability(): Promise<boolean>
setAndroidScreenshotPrivacy(enable: boolean): Promise<void>
getMobileScreenshotPrivacyEnabled(): Promise<boolean | undefined>
setMobileScreenshotPrivacyEnabled(isEnabled: boolean): Promise<void>
}

View File

@@ -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<boolean | undefined> {
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') {

View File

@@ -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<ProtectionEvent> 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<ProtectionEvent> 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<MobileUnlockTiming | undefined> {
return this.storageService.getValue<Promise<MobileUnlockTiming | undefined>>(
StorageKey.MobileBiometricsTiming,
StorageValueModes.Nonwrapped,
)
}
private async getPasscodeTiming(): Promise<MobileUnlockTiming | undefined> {
return this.storageService.getValue<Promise<MobileUnlockTiming | undefined>>(
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<boolean | undefined> {
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<ProtectionEvent> 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,

View File

@@ -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.')
}

View File

@@ -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,
)

View File

@@ -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 (
<div>
<PreferencesGroup>
<PreferencesSegment>
<Title>Biometrics Lock</Title>
<Button className={'mt-1'} label={biometricTitle} onClick={onBiometricsPress} primary />
{hasBiometrics && (
<div className="mt-2 flex flex-row items-center">
<div className={'mr-3'}>Require Biometrics</div>
{biometricsTimingOptions.map((option) => {
return (
<a
key={option.key}
className={classNames(
'mr-3 cursor-pointer rounded px-1.5 py-0.5',
option.selected ? 'bg-info text-info-contrast' : 'text-info',
)}
onClick={() => {
void setBiometricsTimingValue(option.key as MobileUnlockTiming)
}}
>
{option.title}
</a>
)
})}
</div>
)}
</PreferencesSegment>
</PreferencesGroup>
</div>
)
}
export default observer(BiometricsLock)

View File

@@ -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<boolean | undefined>(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 (
<div>
<PreferencesGroup>
<PreferencesSegment>
<Title>{screenshotPrivacyFeatureText}</Title>
<Button className={'mt-1'} label={screenshotPrivacyTitle} onClick={onScreenshotPrivacyPress} primary />
</PreferencesSegment>
</PreferencesGroup>
</div>
)
}
export default observer(MultitaskingPrivacy)

View File

@@ -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<SecurityProps> = (props) => (
<PreferencesPane>
<Encryption viewControllerManager={props.viewControllerManager} />
{props.application.items.invalidItems.length > 0 && (
<ErroredItems viewControllerManager={props.viewControllerManager} />
)}
<Protections application={props.application} />
<TwoFactorAuthWrapper mfaProvider={props.mfaProvider} userProvider={props.userProvider} />
<PasscodeLock viewControllerManager={props.viewControllerManager} application={props.application} />
{props.application.getUser() && <Privacy application={props.application} />}
</PreferencesPane>
)
const SHOW_MULTITASKING_PRIVACY = false
const SHOW_BIOMETRICS_LOCK = false
const Security: FunctionComponent<SecurityProps> = (props) => {
const isNativeMobileWeb = props.application.isNativeMobileWeb()
return (
<PreferencesPane>
<Encryption viewControllerManager={props.viewControllerManager} />
{props.application.items.invalidItems.length > 0 && (
<ErroredItems viewControllerManager={props.viewControllerManager} />
)}
<Protections application={props.application} />
<TwoFactorAuthWrapper mfaProvider={props.mfaProvider} userProvider={props.userProvider} />
{SHOW_MULTITASKING_PRIVACY && isNativeMobileWeb && <MultitaskingPrivacy application={props.application} />}
<PasscodeLock viewControllerManager={props.viewControllerManager} application={props.application} />
{SHOW_BIOMETRICS_LOCK && isNativeMobileWeb && <BiometricsLock application={props.application} />}
{props.application.getUser() && <Privacy application={props.application} />}
</PreferencesPane>
)
}
export default Security