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:
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user