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:
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}.`)
|
||||
|
||||
@@ -197,7 +197,7 @@ export const Authenticate = ({
|
||||
state: AuthenticationValueStateType.Pending,
|
||||
})
|
||||
|
||||
if (application?.getAppState().screenshotPrivacyEnabled) {
|
||||
if (await application?.getMobileScreenshotPrivacyEnabled()) {
|
||||
hide()
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user