feat: soft biometrics lock (#1793)
This commit is contained in:
@@ -30,6 +30,7 @@ export interface ApplicationInterface {
|
|||||||
createDecryptedBackupFile(): Promise<BackupFile | undefined>
|
createDecryptedBackupFile(): Promise<BackupFile | undefined>
|
||||||
hasPasscode(): boolean
|
hasPasscode(): boolean
|
||||||
lock(): Promise<void>
|
lock(): Promise<void>
|
||||||
|
softLockBiometrics(): void
|
||||||
setValue(key: string, value: unknown, mode?: StorageValueModes): void
|
setValue(key: string, value: unknown, mode?: StorageValueModes): void
|
||||||
getValue(key: string, mode?: StorageValueModes): unknown
|
getValue(key: string, mode?: StorageValueModes): unknown
|
||||||
removeValue(key: string, mode?: StorageValueModes): Promise<void>
|
removeValue(key: string, mode?: StorageValueModes): Promise<void>
|
||||||
|
|||||||
@@ -62,4 +62,6 @@ export enum ApplicationEvent {
|
|||||||
UnprotectedSessionExpired = 29,
|
UnprotectedSessionExpired = 29,
|
||||||
/** Called when the app first launches and after first sync request made after sign in */
|
/** Called when the app first launches and after first sync request made after sign in */
|
||||||
CompletedInitialSync = 30,
|
CompletedInitialSync = 30,
|
||||||
|
BiometricsSoftLockEngaged = 31,
|
||||||
|
BiometricsSoftLockDisengaged = 32,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import {
|
|||||||
SubscriptionManager,
|
SubscriptionManager,
|
||||||
WorkspaceClientInterface,
|
WorkspaceClientInterface,
|
||||||
WorkspaceManager,
|
WorkspaceManager,
|
||||||
|
ChallengePrompt,
|
||||||
} from '@standardnotes/services'
|
} from '@standardnotes/services'
|
||||||
import { FilesClientInterface } from '@standardnotes/files'
|
import { FilesClientInterface } from '@standardnotes/files'
|
||||||
import { ComputePrivateUsername } from '@standardnotes/encryption'
|
import { ComputePrivateUsername } from '@standardnotes/encryption'
|
||||||
@@ -929,6 +930,24 @@ export class SNApplication
|
|||||||
return this.deinit(this.getDeinitMode(), DeinitSource.Lock)
|
return this.deinit(this.getDeinitMode(), DeinitSource.Lock)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public softLockBiometrics(): void {
|
||||||
|
const challenge = new Challenge(
|
||||||
|
[new ChallengePrompt(ChallengeValidation.Biometric)],
|
||||||
|
ChallengeReason.ApplicationUnlock,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
|
void this.promptForCustomChallenge(challenge)
|
||||||
|
|
||||||
|
void this.notifyEvent(ApplicationEvent.BiometricsSoftLockEngaged)
|
||||||
|
|
||||||
|
this.addChallengeObserver(challenge, {
|
||||||
|
onComplete: () => {
|
||||||
|
void this.notifyEvent(ApplicationEvent.BiometricsSoftLockDisengaged)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
isNativeMobileWeb() {
|
isNativeMobileWeb() {
|
||||||
return this.environment === Environment.NativeMobileWeb
|
return this.environment === Environment.NativeMobileWeb
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import {
|
|||||||
ThemeManager,
|
ThemeManager,
|
||||||
WebAlertService,
|
WebAlertService,
|
||||||
} from '@standardnotes/ui-services'
|
} from '@standardnotes/ui-services'
|
||||||
import { MobileWebReceiver } from './MobileWebReceiver'
|
import { MobileWebReceiver } from '../NativeMobileWeb/MobileWebReceiver'
|
||||||
import { AndroidBackHandler } from '@/NativeMobileWeb/AndroidBackHandler'
|
import { AndroidBackHandler } from '@/NativeMobileWeb/AndroidBackHandler'
|
||||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||||
import { setViewportHeightWithFallback } from '@/setViewportHeightWithFallback'
|
import { setViewportHeightWithFallback } from '@/setViewportHeightWithFallback'
|
||||||
@@ -289,8 +289,10 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
|||||||
const passcodeLockImmediately = hasPasscode && passcodeTiming === MobileUnlockTiming.Immediately
|
const passcodeLockImmediately = hasPasscode && passcodeTiming === MobileUnlockTiming.Immediately
|
||||||
const biometricsLockImmediately = hasBiometrics && biometricsTiming === MobileUnlockTiming.Immediately
|
const biometricsLockImmediately = hasBiometrics && biometricsTiming === MobileUnlockTiming.Immediately
|
||||||
|
|
||||||
if (passcodeLockImmediately || biometricsLockImmediately) {
|
if (passcodeLockImmediately) {
|
||||||
await this.lock()
|
await this.lock()
|
||||||
|
} else if (biometricsLockImmediately) {
|
||||||
|
this.softLockBiometrics()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -121,6 +121,10 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
|||||||
alertDialog({
|
alertDialog({
|
||||||
text: 'Unable to write to local database. Please restart the app and try again.',
|
text: 'Unable to write to local database. Please restart the app and try again.',
|
||||||
}).catch(console.error)
|
}).catch(console.error)
|
||||||
|
} else if (eventName === ApplicationEvent.BiometricsSoftLockEngaged) {
|
||||||
|
setNeedsUnlock(true)
|
||||||
|
} else if (eventName === ApplicationEvent.BiometricsSoftLockDisengaged) {
|
||||||
|
setNeedsUnlock(false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -197,7 +201,7 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
|||||||
return (
|
return (
|
||||||
<AndroidBackHandlerProvider application={application}>
|
<AndroidBackHandlerProvider application={application}>
|
||||||
<DarkModeHandler application={application} />
|
<DarkModeHandler application={application} />
|
||||||
<ResponsivePaneProvider>
|
<ResponsivePaneProvider paneController={application.getViewControllerManager().paneController}>
|
||||||
<PremiumModalProvider application={application} viewControllerManager={viewControllerManager}>
|
<PremiumModalProvider application={application} viewControllerManager={viewControllerManager}>
|
||||||
<div className={platformString + ' main-ui-view sn-component'}>
|
<div className={platformString + ' main-ui-view sn-component'}>
|
||||||
<div id="app" className="app app-column-container" ref={appColumnContainerRef}>
|
<div id="app" className="app app-column-container" ref={appColumnContainerRef}>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { ElementIds } from '@/Constants/ElementIDs'
|
import { ElementIds } from '@/Constants/ElementIDs'
|
||||||
import { useAndroidBackHandler } from '@/NativeMobileWeb/useAndroidBackHandler'
|
import { useAndroidBackHandler } from '@/NativeMobileWeb/useAndroidBackHandler'
|
||||||
import { isMobileScreen } from '@/Utils'
|
|
||||||
import {
|
import {
|
||||||
useEffect,
|
useEffect,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
@@ -15,6 +14,8 @@ import {
|
|||||||
MutableRefObject,
|
MutableRefObject,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { AppPaneId } from './AppPaneMetadata'
|
import { AppPaneId } from './AppPaneMetadata'
|
||||||
|
import { PaneController } from '../../Controllers/PaneController'
|
||||||
|
import { observer } from 'mobx-react-lite'
|
||||||
|
|
||||||
type ResponsivePaneData = {
|
type ResponsivePaneData = {
|
||||||
selectedPane: AppPaneId
|
selectedPane: AppPaneId
|
||||||
@@ -39,6 +40,10 @@ type ChildrenProps = {
|
|||||||
children: ReactNode
|
children: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProviderProps = {
|
||||||
|
paneController: PaneController
|
||||||
|
} & ChildrenProps
|
||||||
|
|
||||||
function useStateRef<State>(state: State): MutableRefObject<State> {
|
function useStateRef<State>(state: State): MutableRefObject<State> {
|
||||||
const ref = useRef<State>(state)
|
const ref = useRef<State>(state)
|
||||||
|
|
||||||
@@ -51,21 +56,17 @@ function useStateRef<State>(state: State): MutableRefObject<State> {
|
|||||||
|
|
||||||
const MemoizedChildren = memo(({ children }: ChildrenProps) => <div>{children}</div>)
|
const MemoizedChildren = memo(({ children }: ChildrenProps) => <div>{children}</div>)
|
||||||
|
|
||||||
const ResponsivePaneProvider = ({ children }: ChildrenProps) => {
|
const ResponsivePaneProvider = ({ paneController, children }: ProviderProps) => {
|
||||||
const [currentSelectedPane, setCurrentSelectedPane] = useState<AppPaneId>(
|
const currentSelectedPane = paneController.currentPane
|
||||||
isMobileScreen() ? AppPaneId.Items : AppPaneId.Editor,
|
const previousSelectedPane = paneController.previousPane
|
||||||
)
|
|
||||||
const currentSelectedPaneRef = useStateRef<AppPaneId>(currentSelectedPane)
|
const currentSelectedPaneRef = useStateRef<AppPaneId>(currentSelectedPane)
|
||||||
const [previousSelectedPane, setPreviousSelectedPane] = useState<AppPaneId>(
|
|
||||||
isMobileScreen() ? AppPaneId.Items : AppPaneId.Editor,
|
|
||||||
)
|
|
||||||
|
|
||||||
const toggleAppPane = useCallback(
|
const toggleAppPane = useCallback(
|
||||||
(paneId: AppPaneId) => {
|
(paneId: AppPaneId) => {
|
||||||
setPreviousSelectedPane(currentSelectedPane)
|
paneController.setPreviousPane(currentSelectedPane)
|
||||||
setCurrentSelectedPane(paneId)
|
paneController.setCurrentPane(paneId)
|
||||||
},
|
},
|
||||||
[currentSelectedPane],
|
[paneController, currentSelectedPane],
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -122,4 +123,4 @@ const ResponsivePaneProvider = ({ children }: ChildrenProps) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ResponsivePaneProvider
|
export default observer(ResponsivePaneProvider)
|
||||||
|
|||||||
26
packages/web/src/javascripts/Controllers/PaneController.ts
Normal file
26
packages/web/src/javascripts/Controllers/PaneController.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { AppPaneId } from './../Components/ResponsivePane/AppPaneMetadata'
|
||||||
|
import { isMobileScreen } from '@/Utils'
|
||||||
|
import { makeObservable, observable, action } from 'mobx'
|
||||||
|
|
||||||
|
export class PaneController {
|
||||||
|
currentPane: AppPaneId = isMobileScreen() ? AppPaneId.Items : AppPaneId.Editor
|
||||||
|
previousPane: AppPaneId = isMobileScreen() ? AppPaneId.Items : AppPaneId.Editor
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
makeObservable(this, {
|
||||||
|
currentPane: observable,
|
||||||
|
previousPane: observable,
|
||||||
|
|
||||||
|
setCurrentPane: action,
|
||||||
|
setPreviousPane: action,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentPane(pane: AppPaneId): void {
|
||||||
|
this.currentPane = pane
|
||||||
|
}
|
||||||
|
|
||||||
|
setPreviousPane(pane: AppPaneId): void {
|
||||||
|
this.previousPane = pane
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { PaneController } from './PaneController'
|
||||||
import { RouteType, storage, StorageKey } from '@standardnotes/ui-services'
|
import { RouteType, storage, StorageKey } from '@standardnotes/ui-services'
|
||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController'
|
import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController'
|
||||||
@@ -56,6 +57,7 @@ export class ViewControllerManager {
|
|||||||
readonly selectionController: SelectedItemsController
|
readonly selectionController: SelectedItemsController
|
||||||
readonly historyModalController: HistoryModalController
|
readonly historyModalController: HistoryModalController
|
||||||
readonly linkingController: LinkingController
|
readonly linkingController: LinkingController
|
||||||
|
readonly paneController: PaneController
|
||||||
|
|
||||||
public isSessionsModalVisible = false
|
public isSessionsModalVisible = false
|
||||||
|
|
||||||
@@ -71,6 +73,8 @@ export class ViewControllerManager {
|
|||||||
|
|
||||||
this.subscriptionManager = application.subscriptions
|
this.subscriptionManager = application.subscriptions
|
||||||
|
|
||||||
|
this.paneController = new PaneController()
|
||||||
|
|
||||||
this.preferencesController = new PreferencesController(application, this.eventBus)
|
this.preferencesController = new PreferencesController(application, this.eventBus)
|
||||||
|
|
||||||
this.selectionController = new SelectedItemsController(application, this.eventBus)
|
this.selectionController = new SelectedItemsController(application, this.eventBus)
|
||||||
@@ -207,6 +211,7 @@ export class ViewControllerManager {
|
|||||||
|
|
||||||
this.historyModalController.deinit()
|
this.historyModalController.deinit()
|
||||||
;(this.historyModalController as unknown) = undefined
|
;(this.historyModalController as unknown) = undefined
|
||||||
|
;(this.paneController as unknown) = undefined
|
||||||
|
|
||||||
destroyAllObjectProperties(this)
|
destroyAllObjectProperties(this)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user