feat: mobile web bridge (#1597)

This commit is contained in:
Mo
2022-09-19 14:47:15 -05:00
committed by GitHub
parent f80cc5b822
commit c4d7761496
28 changed files with 462 additions and 104 deletions

View File

@@ -23,13 +23,13 @@ import { BackupsService } from './BackupsService'
import { ComponentManager } from './ComponentManager'
import { FilesService } from './FilesService'
import { InstallationService } from './InstallationService'
import { MobileDeviceInterface } from './Interface'
import { MobileDevice } from './Interface'
import { push } from './NavigationService'
import { PreferencesManager } from './PreferencesManager'
import { SNReactNativeCrypto } from './ReactNativeCrypto'
import { ReviewService } from './ReviewService'
import { StatusManager } from './StatusManager'
import { IsDev } from './Utils'
import { IsDev, IsMobileWeb } from './Utils'
type MobileServices = {
applicationState: ApplicationState
@@ -52,7 +52,7 @@ export class MobileApplication extends SNApplication {
static previouslyLaunched = false
constructor(deviceInterface: MobileDeviceInterface, identifier: string) {
constructor(deviceInterface: MobileDevice, identifier: string) {
super({
environment: Environment.Mobile,
platform: platformFromString(Platform.OS),
@@ -135,7 +135,7 @@ export class MobileApplication extends SNApplication {
}
promptForChallenge(challenge: Challenge) {
if (IsDev) {
if (IsMobileWeb) {
return
}

View File

@@ -4,14 +4,14 @@ import { ApplicationState } from './ApplicationState'
import { BackupsService } from './BackupsService'
import { FilesService } from './FilesService'
import { InstallationService } from './InstallationService'
import { MobileDeviceInterface } from './Interface'
import { MobileDevice } from './Interface'
import { PreferencesManager } from './PreferencesManager'
import { ReviewService } from './ReviewService'
import { StatusManager } from './StatusManager'
export class ApplicationGroup extends SNApplicationGroup {
constructor() {
super(new MobileDeviceInterface())
super(new MobileDevice())
}
override async initialize(_callback?: any): Promise<void> {
@@ -21,7 +21,7 @@ export class ApplicationGroup extends SNApplicationGroup {
}
private createApplication = async (descriptor: ApplicationDescriptor, deviceInterface: DeviceInterface) => {
const application = new MobileApplication(deviceInterface as MobileDeviceInterface, descriptor.identifier)
const application = new MobileApplication(deviceInterface as MobileDevice, descriptor.identifier)
const internalEventBus = new InternalEventBus()
const applicationState = new ApplicationState(application)
const reviewService = new ReviewService(application, internalEventBus)

View File

@@ -1,4 +1,4 @@
import { MobileDeviceInterface } from '@Lib/Interface'
import { MobileDevice } from '@Lib/Interface'
import {
ApplicationEvent,
ApplicationService,
@@ -169,9 +169,7 @@ export class ApplicationState extends ApplicationService {
override async onAppLaunch() {
MobileApplication.setPreviouslyLaunched()
this.screenshotPrivacyEnabled = (await this.getScreenshotPrivacyEnabled()) ?? true
await (this.application.deviceInterface as MobileDeviceInterface).setAndroidScreenshotPrivacy(
this.screenshotPrivacyEnabled,
)
await (this.application.deviceInterface as MobileDevice).setAndroidScreenshotPrivacy(this.screenshotPrivacyEnabled)
}
/**
@@ -480,28 +478,35 @@ export class ApplicationState extends ApplicationService {
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 === MobileUnlockTiming.Immediately) {
await this.application.lock()
} else if (hasBiometrics && this.biometricsTiming === MobileUnlockTiming.Immediately && !this.locked) {
const challenge = new Challenge(
[new ChallengePrompt(ChallengeValidation.Biometric)],
ChallengeReason.ApplicationUnlock,
false,
)
void this.application.promptForCustomChallenge(challenge)
this.locked = true
this.notifyLockStateObservers(LockStateType.Locked)
this.application.addChallengeObserver(challenge, {
onComplete: () => {
this.locked = false
this.notifyLockStateObservers(LockStateType.Unlocked)
},
})
}
if (isLocked) {
return
}
const hasBiometrics = this.application.hasBiometrics()
const hasPasscode = this.application.hasPasscode()
const passcodeLockImmediately = hasPasscode && this.passcodeTiming === MobileUnlockTiming.Immediately
const biometricsLockImmediately =
hasBiometrics && this.biometricsTiming === MobileUnlockTiming.Immediately && !this.locked
if (passcodeLockImmediately) {
await this.application.lock()
} else if (biometricsLockImmediately) {
const challenge = new Challenge(
[new ChallengePrompt(ChallengeValidation.Biometric)],
ChallengeReason.ApplicationUnlock,
false,
)
void this.application.promptForCustomChallenge(challenge)
this.locked = true
this.notifyLockStateObservers(LockStateType.Locked)
this.application.addChallengeObserver(challenge, {
onComplete: () => {
this.locked = false
this.notifyLockStateObservers(LockStateType.Unlocked)
},
})
}
}
@@ -570,30 +575,26 @@ export class ApplicationState extends ApplicationService {
}
private async getPasscodeTiming(): Promise<MobileUnlockTiming | undefined> {
return this.application.getValue(StorageKey.MobilePasscodeTiming, StorageValueModes.Nonwrapped) as Promise<
MobileUnlockTiming | undefined
>
return this.application.getMobilePasscodeTiming()
}
private async getBiometricsTiming(): Promise<MobileUnlockTiming | undefined> {
return this.application.getValue(StorageKey.MobileBiometricsTiming, StorageValueModes.Nonwrapped) as Promise<
MobileUnlockTiming | undefined
>
return this.application.getMobileBiometricsTiming()
}
public async setScreenshotPrivacyEnabled(enabled: boolean) {
await this.application.setMobileScreenshotPrivacyEnabled(enabled)
this.screenshotPrivacyEnabled = enabled
await (this.application.deviceInterface as MobileDeviceInterface).setAndroidScreenshotPrivacy(enabled)
await (this.application.deviceInterface as MobileDevice).setAndroidScreenshotPrivacy(enabled)
}
public async setPasscodeTiming(timing: MobileUnlockTiming) {
await this.application.setValue(StorageKey.MobilePasscodeTiming, timing, StorageValueModes.Nonwrapped)
this.application.setValue(StorageKey.MobilePasscodeTiming, timing, StorageValueModes.Nonwrapped)
this.passcodeTiming = timing
}
public async setBiometricsTiming(timing: MobileUnlockTiming) {
await this.application.setValue(StorageKey.MobileBiometricsTiming, timing, StorageValueModes.Nonwrapped)
this.application.setValue(StorageKey.MobileBiometricsTiming, timing, StorageValueModes.Nonwrapped)
this.biometricsTiming = timing
}
@@ -605,7 +606,7 @@ export class ApplicationState extends ApplicationService {
}
public async setPasscodeKeyboardType(type: PasscodeKeyboardType) {
await this.application.setValue(MobileStorageKey.PasscodeKeyboardTypeKey, type, StorageValueModes.Nonwrapped)
this.application.setValue(MobileStorageKey.PasscodeKeyboardTypeKey, type, StorageValueModes.Nonwrapped)
}
public onDrawerOpen() {

View File

@@ -1,6 +1,6 @@
import SNReactNative from '@standardnotes/react-native-utils'
import { ApplicationService, ButtonType, StorageValueModes } from '@standardnotes/snjs'
import { MobileDeviceInterface } from './Interface'
import { MobileDevice } from './Interface'
const FIRST_RUN_KEY = 'first_run'
@@ -23,7 +23,7 @@ export class InstallationService extends ApplicationService {
*/
async needsWipe() {
const hasAccountOrPasscode = this.application.hasAccount() || this.application?.hasPasscode()
const deviceInterface = this.application.deviceInterface as MobileDeviceInterface
const deviceInterface = this.application.deviceInterface as MobileDevice
const keychainKey = await deviceInterface.getNamespacedKeychainValue(this.application.identifier)
const hasKeychainValue = keychainKey != undefined

View File

@@ -2,21 +2,31 @@ import AsyncStorage from '@react-native-community/async-storage'
import SNReactNative from '@standardnotes/react-native-utils'
import {
ApplicationIdentifier,
DeviceInterface,
Environment,
LegacyMobileKeychainStructure,
LegacyRawKeychainValue,
MobileDeviceInterface,
NamespacedRootKeyInKeychain,
RawKeychainValue,
removeFromArray,
TransferPayload,
} 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 { hide, show } from 'react-native-privacy-snapshot'
import { AppStateObserverService } from './../AppStateObserverService'
import Keychain from './Keychain'
import { IsMobileWeb } from './Utils'
export type BiometricsType = 'Fingerprint' | 'Face ID' | 'Biometrics' | 'Touch ID'
export enum MobileDeviceEvent {
RequestsWebViewReload = 0,
}
type MobileDeviceEventHandler = (event: MobileDeviceEvent) => void
/**
* This identifier was the database name used in Standard Notes web/desktop.
*/
@@ -52,11 +62,16 @@ const showLoadFailForItemIds = (failedItemIds: string[]) => {
Alert.alert('Unable to load item(s)', text)
}
export class MobileDeviceInterface implements DeviceInterface {
export class MobileDevice implements MobileDeviceInterface {
environment: Environment.Mobile = Environment.Mobile
private eventObservers: MobileDeviceEventHandler[] = []
// eslint-disable-next-line @typescript-eslint/no-empty-function
deinit() {}
constructor(private stateObserverService?: AppStateObserverService) {}
deinit() {
this.stateObserverService?.deinit()
;(this.stateObserverService as unknown) = undefined
}
async setLegacyRawKeychainValue(value: LegacyRawKeychainValue): Promise<void> {
await Keychain.setKeys(value)
@@ -177,6 +192,14 @@ export class MobileDeviceInterface implements DeviceInterface {
}
}
hideMobileInterfaceFromScreenshots(): void {
hide()
}
stopHidingMobileInterfaceFromScreenshots(): void {
show()
}
async getAllRawStorageKeyValues() {
const keys = await AsyncStorage.getAllKeys()
return this.getRawStorageKeyValues(keys)
@@ -288,8 +311,10 @@ export class MobileDeviceInterface implements DeviceInterface {
}
}
authenticateWithBiometrics() {
return new Promise<boolean>((resolve) => {
async authenticateWithBiometrics() {
this.stateObserverService?.beginIgnoringStateChanges()
const result = await new Promise<boolean>((resolve) => {
if (Platform.OS === 'android') {
FingerprintScanner.authenticate({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -333,10 +358,20 @@ export class MobileDeviceInterface implements DeviceInterface {
})
}
})
this.stateObserverService?.stopIgnoringStateChanges()
return result
}
getRawKeychainValue(): Promise<RawKeychainValue | null | undefined> {
return Keychain.getKeys()
async getRawKeychainValue(): Promise<RawKeychainValue | undefined> {
const result = await Keychain.getKeys()
if (result === null) {
return undefined
}
return result
}
async clearRawKeychainValue(): Promise<void> {
@@ -375,7 +410,27 @@ export class MobileDeviceInterface implements DeviceInterface {
}
performSoftReset() {
SNReactNative.exitApp()
if (IsMobileWeb) {
this.notifyEvent(MobileDeviceEvent.RequestsWebViewReload)
} else {
SNReactNative.exitApp()
}
}
addMobileWebEventReceiver(handler: MobileDeviceEventHandler): () => void {
this.eventObservers.push(handler)
const thislessObservers = this.eventObservers
return () => {
removeFromArray(thislessObservers, handler)
}
}
private notifyEvent(event: MobileDeviceEvent): void {
for (const handler of this.eventObservers) {
handler(event)
}
}
// eslint-disable-next-line @typescript-eslint/no-empty-function

View File

@@ -1,7 +1,8 @@
import { TEnvironment } from '@Root/App'
import { TEnvironment } from '@Root/NativeApp'
import VersionInfo from 'react-native-version-info'
export const IsDev = VersionInfo.bundleIdentifier?.includes('dev')
export const IsMobileWeb = IsDev
export function isNullOrUndefined(value: unknown) {
return value === null || value === undefined