feat: mobile web bridge (#1597)
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
import { IsMobileWeb } from '@Lib/Utils'
|
||||
import { MobileWebApp } from '@Root/MobileWebApp'
|
||||
import { SNLog } from '@standardnotes/snjs'
|
||||
import { AppRegistry } from 'react-native'
|
||||
import 'react-native-gesture-handler'
|
||||
import { enableScreens } from 'react-native-screens'
|
||||
import 'react-native-url-polyfill/auto'
|
||||
import { name as appName } from './app.json'
|
||||
import { App } from './src/App'
|
||||
import { NativeApp } from './src/NativeApp'
|
||||
import { enableAndroidFontFix } from './src/Style/android_text_fix'
|
||||
|
||||
enableScreens()
|
||||
@@ -28,11 +30,11 @@ console.warn = function filterWarnings(msg) {
|
||||
"[react-native-gesture-handler] Seems like you're using an old API with gesture components",
|
||||
]
|
||||
|
||||
if (!supressedWarnings.some(entry => msg.includes(entry))) {
|
||||
if (!supressedWarnings.some((entry) => msg.includes(entry))) {
|
||||
originalWarn.apply(console, arguments)
|
||||
}
|
||||
}
|
||||
|
||||
enableAndroidFontFix()
|
||||
|
||||
AppRegistry.registerComponent(appName, () => App)
|
||||
AppRegistry.registerComponent(appName, () => (IsMobileWeb ? MobileWebApp : NativeApp))
|
||||
|
||||
@@ -2,7 +2,7 @@ import { AppStateEventType, AppStateType, TabletModeChangeData } from '@Lib/Appl
|
||||
import { AlwaysOpenWebAppOnLaunchKey } from '@Lib/constants'
|
||||
import { useHasEditor, useIsLocked } from '@Lib/SnjsHelperHooks'
|
||||
import { ScreenStatus } from '@Lib/StatusManager'
|
||||
import { IsDev } from '@Lib/Utils'
|
||||
import { IsMobileWeb } from '@Lib/Utils'
|
||||
import { CompositeNavigationProp, RouteProp } from '@react-navigation/native'
|
||||
import { createStackNavigator, StackNavigationProp } from '@react-navigation/stack'
|
||||
import { HeaderTitleView } from '@Root/Components/HeaderTitleView'
|
||||
@@ -22,10 +22,10 @@ import { Dimensions, Keyboard, ScaledSize, StatusBar } from 'react-native'
|
||||
import DrawerLayout, { DrawerState } from 'react-native-gesture-handler/DrawerLayout'
|
||||
import { HeaderButtons, Item } from 'react-navigation-header-buttons'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { HeaderTitleParams } from './App'
|
||||
import { ApplicationContext } from './ApplicationContext'
|
||||
import { MobileWebAppContainer } from './MobileWebAppContainer'
|
||||
import { ModalStackNavigationProp } from './ModalStack'
|
||||
import { HeaderTitleParams } from './NativeApp'
|
||||
|
||||
export type AppStackNavigatorParamList = {
|
||||
[SCREEN_NOTES]: HeaderTitleParams
|
||||
@@ -121,7 +121,7 @@ export const AppStackComponent = (props: ModalStackNavigationProp<'AppStack'>) =
|
||||
[application],
|
||||
)
|
||||
|
||||
if (IsDev) {
|
||||
if (IsMobileWeb) {
|
||||
return (
|
||||
<AppStack.Navigator
|
||||
screenOptions={() => ({
|
||||
|
||||
62
packages/mobile/src/AppStateObserverService.ts
Normal file
62
packages/mobile/src/AppStateObserverService.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { AbstractService, InternalEventBus, ReactNativeToWebEvent } from '@standardnotes/snjs'
|
||||
import { AppState, AppStateStatus, NativeEventSubscription } from 'react-native'
|
||||
|
||||
export class AppStateObserverService extends AbstractService<ReactNativeToWebEvent> {
|
||||
private mostRecentState?: ReactNativeToWebEvent
|
||||
private removeListener: NativeEventSubscription
|
||||
private ignoringStateChanges = false
|
||||
|
||||
constructor() {
|
||||
const bus = new InternalEventBus()
|
||||
super(bus)
|
||||
|
||||
this.removeListener = AppState.addEventListener('change', async (nextAppState: AppStateStatus) => {
|
||||
if (this.ignoringStateChanges) {
|
||||
return
|
||||
}
|
||||
|
||||
// if the most recent state is not 'background' ('inactive'), then we're going
|
||||
// from inactive to active, which doesn't really happen unless you, say, swipe
|
||||
// notification center in iOS down then back up. We don't want to lock on this state change.
|
||||
const isResuming = nextAppState === 'active'
|
||||
const isResumingFromBackground = isResuming && this.mostRecentState === ReactNativeToWebEvent.EnteringBackground
|
||||
const isEnteringBackground = nextAppState === 'background'
|
||||
const isLosingFocus = nextAppState === 'inactive'
|
||||
|
||||
if (isEnteringBackground) {
|
||||
this.notifyStateChange(ReactNativeToWebEvent.EnteringBackground)
|
||||
}
|
||||
|
||||
if (isResumingFromBackground || isResuming) {
|
||||
if (isResumingFromBackground) {
|
||||
this.notifyStateChange(ReactNativeToWebEvent.ResumingFromBackground)
|
||||
}
|
||||
|
||||
// Notify of GainingFocus even if resuming from background
|
||||
this.notifyStateChange(ReactNativeToWebEvent.GainingFocus)
|
||||
return
|
||||
}
|
||||
|
||||
if (isLosingFocus) {
|
||||
this.notifyStateChange(ReactNativeToWebEvent.LosingFocus)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
beginIgnoringStateChanges() {
|
||||
this.ignoringStateChanges = true
|
||||
}
|
||||
|
||||
stopIgnoringStateChanges() {
|
||||
this.ignoringStateChanges = false
|
||||
}
|
||||
|
||||
deinit() {
|
||||
this.removeListener.remove()
|
||||
}
|
||||
|
||||
private notifyStateChange(state: ReactNativeToWebEvent): void {
|
||||
this.mostRecentState = state
|
||||
void this.notifyEvent(state)
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import React, { useContext } from 'react'
|
||||
import { Platform } from 'react-native'
|
||||
import { HeaderButtons, Item } from 'react-navigation-header-buttons'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { HeaderTitleParams } from './App'
|
||||
import { HeaderTitleParams } from './NativeApp'
|
||||
|
||||
type HistoryStackNavigatorParamList = {
|
||||
[SCREEN_NOTE_HISTORY]: (HeaderTitleParams & { noteUuid: string }) | (undefined & { noteUuid: string })
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
16
packages/mobile/src/MobileWebApp.tsx
Normal file
16
packages/mobile/src/MobileWebApp.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { navigationRef } from '@Lib/NavigationService'
|
||||
import { NavigationContainer } from '@react-navigation/native'
|
||||
import React from 'react'
|
||||
import { MobileWebMainStackComponent } from './ModalStack'
|
||||
|
||||
const AppComponent: React.FC = () => {
|
||||
return (
|
||||
<NavigationContainer ref={navigationRef}>
|
||||
<MobileWebMainStackComponent />
|
||||
</NavigationContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export const MobileWebApp = () => {
|
||||
return <AppComponent />
|
||||
}
|
||||
@@ -1,13 +1,50 @@
|
||||
import { MobileDeviceInterface } from '@Lib/Interface'
|
||||
import React, { useMemo, useRef } from 'react'
|
||||
import { MobileDevice, MobileDeviceEvent } from '@Lib/Interface'
|
||||
import { ReactNativeToWebEvent } from '@standardnotes/snjs'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Platform } from 'react-native'
|
||||
import { WebView, WebViewMessageEvent } from 'react-native-webview'
|
||||
import { AppStateObserverService } from './AppStateObserverService'
|
||||
|
||||
const LoggingEnabled = false
|
||||
|
||||
export const MobileWebAppContainer = () => {
|
||||
const sourceUri = (Platform.OS === 'android' ? 'file:///android_asset/' : '') + 'Web.bundle/src/index.html'
|
||||
const webViewRef = useRef<WebView>(null)
|
||||
const [identifier, setIdentifier] = useState(Math.random())
|
||||
|
||||
const destroyAndReload = useCallback(() => {
|
||||
setIdentifier(Math.random())
|
||||
}, [])
|
||||
|
||||
return <MobileWebAppContents key={`${identifier}`} destroyAndReload={destroyAndReload} />
|
||||
}
|
||||
|
||||
const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => void }) => {
|
||||
const webViewRef = useRef<WebView>(null)
|
||||
const sourceUri = (Platform.OS === 'android' ? 'file:///android_asset/' : '') + 'Web.bundle/src/index.html'
|
||||
const stateService = useMemo(() => new AppStateObserverService(), [])
|
||||
const device = useMemo(() => new MobileDevice(stateService), [stateService])
|
||||
|
||||
useEffect(() => {
|
||||
const removeListener = stateService.addEventObserver((event: ReactNativeToWebEvent) => {
|
||||
webViewRef.current?.postMessage(JSON.stringify({ reactNativeEvent: event, messageType: 'event' }))
|
||||
})
|
||||
|
||||
return () => {
|
||||
removeListener()
|
||||
}
|
||||
}, [webViewRef, stateService])
|
||||
|
||||
useEffect(() => {
|
||||
const observer = device.addMobileWebEventReceiver((event) => {
|
||||
if (event === MobileDeviceEvent.RequestsWebViewReload) {
|
||||
destroyAndReload()
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
observer()
|
||||
}
|
||||
}, [device, destroyAndReload])
|
||||
|
||||
const device = useMemo(() => new MobileDeviceInterface(), [])
|
||||
const functions = Object.getOwnPropertyNames(Object.getPrototypeOf(device))
|
||||
|
||||
const baselineFunctions: Record<string, any> = {
|
||||
@@ -28,7 +65,7 @@ export const MobileWebAppContainer = () => {
|
||||
|
||||
stringFunctions += `
|
||||
${functionName}(...args) {
|
||||
return this.sendMessage('${functionName}', args);
|
||||
return this.askReactNativeToInvokeInterfaceMethod('${functionName}', args);
|
||||
}
|
||||
`
|
||||
}
|
||||
@@ -44,8 +81,8 @@ export const MobileWebAppContainer = () => {
|
||||
|
||||
setApplication() {}
|
||||
|
||||
sendMessage(functionName, args) {
|
||||
return this.messageSender.sendMessage(functionName, args)
|
||||
askReactNativeToInvokeInterfaceMethod(functionName, args) {
|
||||
return this.messageSender.askReactNativeToInvokeInterfaceMethod(functionName, args)
|
||||
}
|
||||
|
||||
${stringFunctions}
|
||||
@@ -56,26 +93,18 @@ export const MobileWebAppContainer = () => {
|
||||
class WebProcessMessageSender {
|
||||
constructor() {
|
||||
this.pendingMessages = []
|
||||
window.addEventListener('message', this.handleMessageFromReactNative.bind(this))
|
||||
document.addEventListener('message', this.handleMessageFromReactNative.bind(this))
|
||||
}
|
||||
|
||||
handleMessageFromReactNative(event) {
|
||||
const message = event.data
|
||||
try {
|
||||
const parsed = JSON.parse(message)
|
||||
const { messageId, returnValue } = parsed
|
||||
const pendingMessage = this.pendingMessages.find((m) => m.messageId === messageId)
|
||||
pendingMessage.resolve(returnValue)
|
||||
this.pendingMessages.splice(this.pendingMessages.indexOf(pendingMessage), 1)
|
||||
} catch (error) {
|
||||
console.log('Error parsing message from React Native', message, error)
|
||||
}
|
||||
handleReplyFromReactNative( messageId, returnValue) {
|
||||
const pendingMessage = this.pendingMessages.find((m) => m.messageId === messageId)
|
||||
pendingMessage.resolve(returnValue)
|
||||
this.pendingMessages.splice(this.pendingMessages.indexOf(pendingMessage), 1)
|
||||
}
|
||||
|
||||
sendMessage(functionName, args) {
|
||||
askReactNativeToInvokeInterfaceMethod(functionName, args) {
|
||||
const messageId = Math.random()
|
||||
window.ReactNativeWebView.postMessage(JSON.stringify({ functionName: functionName, args: args, messageId }))
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.pendingMessages.push({
|
||||
messageId,
|
||||
@@ -98,6 +127,25 @@ export const MobileWebAppContainer = () => {
|
||||
const messageSender = new WebProcessMessageSender();
|
||||
window.reactNativeDevice = new WebProcessDeviceInterface(messageSender);
|
||||
|
||||
const handleMessageFromReactNative = (event) => {
|
||||
const message = event.data
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(message)
|
||||
const { messageId, returnValue, messageType } = parsed
|
||||
|
||||
if (messageType === 'reply') {
|
||||
messageSender.handleReplyFromReactNative(messageId, returnValue)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log('Error parsing message from React Native', message, error)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', handleMessageFromReactNative)
|
||||
document.addEventListener('message', handleMessageFromReactNative)
|
||||
|
||||
true;
|
||||
`
|
||||
|
||||
@@ -107,14 +155,18 @@ export const MobileWebAppContainer = () => {
|
||||
const functionData = JSON.parse(message)
|
||||
void onFunctionMessage(functionData.functionName, functionData.messageId, functionData.args)
|
||||
} catch (error) {
|
||||
console.log('onGeneralMessage', JSON.stringify(message))
|
||||
if (LoggingEnabled) {
|
||||
console.log('onGeneralMessage', JSON.stringify(message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onFunctionMessage = async (functionName: string, messageId: string, args: any) => {
|
||||
const returnValue = await (device as any)[functionName](...args)
|
||||
console.log(`Native device function ${functionName} called`)
|
||||
webViewRef.current?.postMessage(JSON.stringify({ messageId, returnValue }))
|
||||
if (LoggingEnabled) {
|
||||
console.log(`Native device function ${functionName} called`)
|
||||
}
|
||||
webViewRef.current?.postMessage(JSON.stringify({ messageId, returnValue, messageType: 'reply' }))
|
||||
}
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
|
||||
@@ -30,11 +30,11 @@ import React, { memo, useContext } from 'react'
|
||||
import { Platform } from 'react-native'
|
||||
import { HeaderButtons, Item } from 'react-navigation-header-buttons'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { HeaderTitleParams, TEnvironment } from './App'
|
||||
import { ApplicationContext } from './ApplicationContext'
|
||||
import { AppStackComponent } from './AppStack'
|
||||
import { HistoryStack } from './HistoryStack'
|
||||
import { MobileWebAppContainer } from './MobileWebAppContainer'
|
||||
import { HeaderTitleParams, TEnvironment } from './NativeApp'
|
||||
|
||||
export type ModalStackNavigatorParamList = {
|
||||
AppStack: undefined
|
||||
@@ -75,7 +75,31 @@ export type ModalStackNavigationProp<T extends keyof ModalStackNavigatorParamLis
|
||||
|
||||
const MainStack = createStackNavigator<ModalStackNavigatorParamList>()
|
||||
|
||||
export const MainStackComponent = ({ env }: { env: TEnvironment }) => {
|
||||
export const MobileWebMainStackComponent = () => {
|
||||
const MemoizedAppStackComponent = memo((props: ModalStackNavigationProp<'AppStack'>) => (
|
||||
<AppStackComponent {...props} />
|
||||
))
|
||||
|
||||
return (
|
||||
<MainStack.Navigator
|
||||
screenOptions={{
|
||||
gestureEnabled: false,
|
||||
presentation: 'modal',
|
||||
}}
|
||||
initialRouteName="AppStack"
|
||||
>
|
||||
<MainStack.Screen
|
||||
name={'AppStack'}
|
||||
options={{
|
||||
headerShown: false,
|
||||
}}
|
||||
component={MemoizedAppStackComponent}
|
||||
/>
|
||||
</MainStack.Navigator>
|
||||
)
|
||||
}
|
||||
|
||||
export const NativeMainStackComponent = ({ env }: { env: TEnvironment }) => {
|
||||
const application = useContext(ApplicationContext)
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ThemeService, ThemeServiceContext } from '@Style/ThemeService'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { ThemeProvider } from 'styled-components/native'
|
||||
import { ApplicationContext } from './ApplicationContext'
|
||||
import { MainStackComponent } from './ModalStack'
|
||||
import { NativeMainStackComponent } from './ModalStack'
|
||||
|
||||
export type HeaderTitleParams = {
|
||||
title?: string
|
||||
@@ -111,7 +111,7 @@ const AppComponent: React.FC<{
|
||||
<ThemeProvider theme={activeTheme}>
|
||||
<ActionSheetProvider>
|
||||
<ThemeServiceContext.Provider value={themeService.current}>
|
||||
<MainStackComponent env={env} />
|
||||
<NativeMainStackComponent env={env} />
|
||||
</ThemeServiceContext.Provider>
|
||||
</ActionSheetProvider>
|
||||
<ToastWrapper />
|
||||
@@ -122,7 +122,7 @@ const AppComponent: React.FC<{
|
||||
)
|
||||
}
|
||||
|
||||
export const App = (props: { env: TEnvironment }) => {
|
||||
export const NativeApp = (props: { env: TEnvironment }) => {
|
||||
const [application, setApplication] = useState<MobileApplication | undefined>()
|
||||
|
||||
const createNewAppGroup = useCallback(() => {
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AppStateType, PasscodeKeyboardType } from '@Lib/ApplicationState'
|
||||
import { MobileDeviceInterface } from '@Lib/Interface'
|
||||
import { MobileDevice } from '@Lib/Interface'
|
||||
import { HeaderHeightContext } from '@react-navigation/elements'
|
||||
import { useFocusEffect } from '@react-navigation/native'
|
||||
import { ApplicationContext } from '@Root/ApplicationContext'
|
||||
@@ -164,7 +164,7 @@ export const Authenticate = ({
|
||||
}, [])
|
||||
|
||||
const checkForBiometrics = useCallback(
|
||||
async () => (application?.deviceInterface as MobileDeviceInterface).getDeviceBiometricsAvailability(),
|
||||
async () => (application?.deviceInterface as MobileDevice).getDeviceBiometricsAvailability(),
|
||||
[application],
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MobileDeviceInterface } from '@Lib/Interface'
|
||||
import { MobileDevice } from '@Lib/Interface'
|
||||
import { useFocusEffect, useNavigation } from '@react-navigation/native'
|
||||
import { ApplicationContext } from '@Root/ApplicationContext'
|
||||
import { ButtonCell } from '@Root/Components/ButtonCell'
|
||||
@@ -53,7 +53,7 @@ export const SecuritySection = (props: Props) => {
|
||||
void getHasBiometrics()
|
||||
const hasBiometricsSupport = async () => {
|
||||
const hasBiometricsAvailable = await (
|
||||
application?.deviceInterface as MobileDeviceInterface
|
||||
application?.deviceInterface as MobileDevice
|
||||
).getDeviceBiometricsAvailability()
|
||||
if (mounted) {
|
||||
setSupportsBiometrics(hasBiometricsAvailable)
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"eslint": "^8.23.1",
|
||||
"eslint-plugin-prettier": "*",
|
||||
"jest": "^28.1.2",
|
||||
"ts-jest": "^28.0.5"
|
||||
"ts-jest": "^28.0.5",
|
||||
"typescript": "*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,4 +5,8 @@ import { ApplicationInterface } from './ApplicationInterface'
|
||||
export interface WebApplicationInterface extends ApplicationInterface {
|
||||
notifyWebEvent(event: WebAppEvent, data?: unknown): void
|
||||
getDesktopService(): DesktopManagerInterface | undefined
|
||||
handleMobileEnteringBackgroundEvent(): Promise<void>
|
||||
handleMobileGainingFocusEvent(): Promise<void>
|
||||
handleMobileLosingFocusEvent(): Promise<void>
|
||||
handleMobileResumingFromBackgroundEvent(): Promise<void>
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ export interface MobileDeviceInterface extends DeviceInterface {
|
||||
getRawKeychainValue(): Promise<RawKeychainValue | undefined>
|
||||
getDeviceBiometricsAvailability(): Promise<boolean>
|
||||
setAndroidScreenshotPrivacy(enable: boolean): Promise<void>
|
||||
getMobileScreenshotPrivacyEnabled(): Promise<boolean | undefined>
|
||||
setMobileScreenshotPrivacyEnabled(isEnabled: boolean): Promise<void>
|
||||
authenticateWithBiometrics(): Promise<boolean>
|
||||
hideMobileInterfaceFromScreenshots(): void
|
||||
stopHidingMobileInterfaceFromScreenshots(): void
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
FileService,
|
||||
SubscriptionClientInterface,
|
||||
SubscriptionManager,
|
||||
StorageValueModes,
|
||||
} from '@standardnotes/services'
|
||||
import { FilesClientInterface } from '@standardnotes/files'
|
||||
import { ComputePrivateWorkspaceIdentifier } from '@standardnotes/encryption'
|
||||
@@ -60,6 +61,7 @@ import { SNLog } from '../Log'
|
||||
import { Challenge, ChallengeResponse } from '../Services'
|
||||
import { ApplicationConstructorOptions, FullyResolvedApplicationOptions } from './Options/ApplicationOptions'
|
||||
import { ApplicationOptionsDefaults } from './Options/Defaults'
|
||||
import { MobileUnlockTiming } from '@Lib/Services/Protection/MobileUnlockTiming'
|
||||
|
||||
/** How often to automatically sync, in milliseconds */
|
||||
const DEFAULT_AUTO_SYNC_INTERVAL = 30_000
|
||||
@@ -927,7 +929,7 @@ export class SNApplication
|
||||
return this.deinit(this.getDeinitMode(), DeinitSource.Lock)
|
||||
}
|
||||
|
||||
async setBiometricsTiming(timing: InternalServices.MobileUnlockTiming) {
|
||||
async setBiometricsTiming(timing: MobileUnlockTiming) {
|
||||
return this.protectionService.setBiometricsTiming(timing)
|
||||
}
|
||||
|
||||
@@ -935,6 +937,18 @@ export class SNApplication
|
||||
return this.protectionService.getMobileScreenshotPrivacyEnabled()
|
||||
}
|
||||
|
||||
async getMobilePasscodeTiming(): Promise<MobileUnlockTiming | undefined> {
|
||||
return this.getValue(StorageKey.MobilePasscodeTiming, StorageValueModes.Nonwrapped) as Promise<
|
||||
MobileUnlockTiming | undefined
|
||||
>
|
||||
}
|
||||
|
||||
async getMobileBiometricsTiming(): Promise<MobileUnlockTiming | undefined> {
|
||||
return this.getValue(StorageKey.MobileBiometricsTiming, StorageValueModes.Nonwrapped) as Promise<
|
||||
MobileUnlockTiming | undefined
|
||||
>
|
||||
}
|
||||
|
||||
async setMobileScreenshotPrivacyEnabled(isEnabled: boolean) {
|
||||
return this.protectionService.setMobileScreenshotPrivacyEnabled(isEnabled)
|
||||
}
|
||||
|
||||
6
packages/snjs/lib/Client/ReactNativeToWebEvent.ts
Normal file
6
packages/snjs/lib/Client/ReactNativeToWebEvent.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export enum ReactNativeToWebEvent {
|
||||
EnteringBackground = 'EnteringBackground',
|
||||
ResumingFromBackground = 'ResumingFromBackground',
|
||||
GainingFocus = 'GainingFocus',
|
||||
LosingFocus = 'LosingFocus',
|
||||
}
|
||||
@@ -2,3 +2,4 @@ export * from './IconsController'
|
||||
export * from './NoteViewController'
|
||||
export * from './FileViewController'
|
||||
export * from './ItemGroupController'
|
||||
export * from './ReactNativeToWebEvent'
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum MobileUnlockTiming {
|
||||
Immediately = 'immediately',
|
||||
OnQuit = 'on-quit',
|
||||
}
|
||||
@@ -18,17 +18,13 @@ import {
|
||||
} from '@standardnotes/services'
|
||||
import { ProtectionsClientInterface } from './ClientInterface'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { MobileUnlockTiming } from './MobileUnlockTiming'
|
||||
|
||||
export enum ProtectionEvent {
|
||||
UnprotectedSessionBegan = 'UnprotectedSessionBegan',
|
||||
UnprotectedSessionExpired = 'UnprotectedSessionExpired',
|
||||
}
|
||||
|
||||
export enum MobileUnlockTiming {
|
||||
Immediately = 'immediately',
|
||||
OnQuit = 'on-quit',
|
||||
}
|
||||
|
||||
export const ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction = 30
|
||||
|
||||
export enum UnprotectedAccessSecondsDuration {
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './ClientInterface'
|
||||
export * from './ProtectionService'
|
||||
export * from './MobileUnlockTiming'
|
||||
|
||||
@@ -17,12 +17,15 @@ import {
|
||||
DecryptedItemInterface,
|
||||
WebAppEvent,
|
||||
WebApplicationInterface,
|
||||
MobileDeviceInterface,
|
||||
MobileUnlockTiming,
|
||||
} from '@standardnotes/snjs'
|
||||
import { makeObservable, observable } from 'mobx'
|
||||
import { PanelResizedData } from '@/Types/PanelResizedData'
|
||||
import { isDesktopApplication } from '@/Utils'
|
||||
import { DesktopManager } from './Device/DesktopManager'
|
||||
import { ArchiveManager, AutolockService, IOService, WebAlertService, ThemeManager } from '@standardnotes/ui-services'
|
||||
import { MobileWebReceiver } from './MobileWebReceiver'
|
||||
|
||||
type WebServices = {
|
||||
viewControllerManager: ViewControllerManager
|
||||
@@ -41,6 +44,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
public itemControllerGroup: ItemGroupController
|
||||
public iconsController: IconsController
|
||||
private onVisibilityChange: () => void
|
||||
private mobileWebReceiver?: MobileWebReceiver
|
||||
|
||||
constructor(
|
||||
deviceInterface: WebOrDesktopDevice,
|
||||
@@ -70,6 +74,10 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
this.itemControllerGroup = new ItemGroupController(this)
|
||||
this.iconsController = new IconsController()
|
||||
|
||||
if (this.isNativeMobileWeb()) {
|
||||
this.mobileWebReceiver = new MobileWebReceiver(this)
|
||||
}
|
||||
|
||||
this.onVisibilityChange = () => {
|
||||
const visible = document.visibilityState === 'visible'
|
||||
const event = visible ? WebAppEvent.WindowDidFocus : WebAppEvent.WindowDidBlur
|
||||
@@ -101,6 +109,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
|
||||
this.itemControllerGroup.deinit()
|
||||
;(this.itemControllerGroup as unknown) = undefined
|
||||
;(this.mobileWebReceiver as unknown) = undefined
|
||||
|
||||
this.webEventObservers.length = 0
|
||||
|
||||
@@ -161,6 +170,13 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
return undefined
|
||||
}
|
||||
|
||||
get mobileDevice(): MobileDeviceInterface {
|
||||
if (!this.isNativeMobileWeb()) {
|
||||
throw Error('Attempting to access device as mobile device on non mobile platform')
|
||||
}
|
||||
return this.deviceInterface as MobileDeviceInterface
|
||||
}
|
||||
|
||||
public getThemeService() {
|
||||
return this.webServices.themeService
|
||||
}
|
||||
@@ -203,4 +219,44 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
const currentValue = this.isGlobalSpellcheckEnabled()
|
||||
return this.setPreference(PrefKey.EditorSpellcheck, !currentValue)
|
||||
}
|
||||
|
||||
async handleMobileEnteringBackgroundEvent(): Promise<void> {
|
||||
await this.lockApplicationAfterMobileEventIfApplicable()
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
async handleMobileGainingFocusEvent(): Promise<void> {}
|
||||
|
||||
async handleMobileLosingFocusEvent(): Promise<void> {
|
||||
if (await this.getMobileScreenshotPrivacyEnabled()) {
|
||||
this.mobileDevice.stopHidingMobileInterfaceFromScreenshots()
|
||||
}
|
||||
|
||||
await this.lockApplicationAfterMobileEventIfApplicable()
|
||||
}
|
||||
|
||||
async handleMobileResumingFromBackgroundEvent(): Promise<void> {
|
||||
if (await this.getMobileScreenshotPrivacyEnabled()) {
|
||||
this.mobileDevice.hideMobileInterfaceFromScreenshots()
|
||||
}
|
||||
}
|
||||
|
||||
private async lockApplicationAfterMobileEventIfApplicable(): Promise<void> {
|
||||
const isLocked = await this.isLocked()
|
||||
if (isLocked) {
|
||||
return
|
||||
}
|
||||
|
||||
const hasBiometrics = this.hasBiometrics()
|
||||
const hasPasscode = this.hasPasscode()
|
||||
const passcodeTiming = await this.getMobilePasscodeTiming()
|
||||
const biometricsTiming = await this.getMobileBiometricsTiming()
|
||||
|
||||
const passcodeLockImmediately = hasPasscode && passcodeTiming === MobileUnlockTiming.Immediately
|
||||
const biometricsLockImmediately = hasBiometrics && biometricsTiming === MobileUnlockTiming.Immediately
|
||||
|
||||
if (passcodeLockImmediately || biometricsLockImmediately) {
|
||||
await this.lock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { ReactNativeToWebEvent, WebApplicationInterface } from '@standardnotes/snjs'
|
||||
|
||||
export class MobileWebReceiver {
|
||||
constructor(private application: WebApplicationInterface) {
|
||||
this.listenForNativeMobileEvents()
|
||||
}
|
||||
|
||||
deinit() {
|
||||
;(this.application as unknown) = undefined
|
||||
window.removeEventListener('message', this.handleNativeMobileWindowMessage)
|
||||
document.removeEventListener('message', this.handleNativeMobileWindowMessage as never)
|
||||
}
|
||||
|
||||
listenForNativeMobileEvents() {
|
||||
const iOSEventRecipient = window
|
||||
const androidEventRecipient = document
|
||||
iOSEventRecipient.addEventListener('message', this.handleNativeMobileWindowMessage)
|
||||
androidEventRecipient.addEventListener('message', this.handleNativeMobileWindowMessage as never)
|
||||
}
|
||||
|
||||
handleNativeMobileWindowMessage = (event: MessageEvent) => {
|
||||
const nullOrigin = event.origin === '' || event.origin == null
|
||||
if (!nullOrigin) {
|
||||
return
|
||||
}
|
||||
|
||||
const message = (event as MessageEvent).data
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(message)
|
||||
const { messageType, reactNativeEvent } = parsed
|
||||
|
||||
if (messageType === 'event' && reactNativeEvent) {
|
||||
const nativeEvent = reactNativeEvent as ReactNativeToWebEvent
|
||||
this.handleNativeEvent(nativeEvent)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error parsing message from React Native', error)
|
||||
}
|
||||
}
|
||||
|
||||
handleNativeEvent(event: ReactNativeToWebEvent) {
|
||||
switch (event) {
|
||||
case ReactNativeToWebEvent.EnteringBackground:
|
||||
void this.application.handleMobileEnteringBackgroundEvent()
|
||||
break
|
||||
case ReactNativeToWebEvent.GainingFocus:
|
||||
void this.application.handleMobileGainingFocusEvent()
|
||||
break
|
||||
case ReactNativeToWebEvent.LosingFocus:
|
||||
void this.application.handleMobileLosingFocusEvent()
|
||||
break
|
||||
case ReactNativeToWebEvent.ResumingFromBackground:
|
||||
void this.application.handleMobileResumingFromBackgroundEvent()
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user