From 545c07ecf3e9c93060861222d97086f5fccbe464 Mon Sep 17 00:00:00 2001 From: Radek Czemerys <7029942+radko93@users.noreply.github.com> Date: Thu, 17 Sep 2020 13:20:42 +0200 Subject: [PATCH] Application Group (#286) * feat: application group * feat: update ApplicationGroup * fix: application state * chore: bump snjs * fix: getDatabaseKeyPrefix * refactor: remove unused stylekit from application Co-authored-by: Johnny Almonte Co-authored-by: Johnny A <5891646+johnny243@users.noreply.github.com> --- package.json | 2 +- src/App.tsx | 21 +++-- src/lib/InstallationService.ts | 8 +- src/lib/application.ts | 17 ++-- src/lib/applicationGroup.ts | 82 +++++-------------- src/lib/interface.ts | 69 ++++++++++------ src/screens/Settings/Sections/AuthSection.tsx | 16 +--- yarn.lock | 4 +- 8 files changed, 97 insertions(+), 122 deletions(-) diff --git a/package.json b/package.json index 80889f85..9d6330b3 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "react-navigation-header-buttons": "^5.0.2", "sn-textview": "standardnotes/sn-textview#f42f0bf", "sncrypto": "standardnotes/sncrypto#5f8cd36", - "snjs": "standardnotes/snjs#d1f61cd", + "snjs": "standardnotes/snjs#9003251", "standard-notes-rn": "standardnotes/standard-notes-rn", "styled-components": "^5.2.0" }, diff --git a/src/App.tsx b/src/App.tsx index 56d12c58..270dd72c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -54,7 +54,12 @@ import DrawerLayout, { DrawerState, } from 'react-native-gesture-handler/DrawerLayout'; import { HeaderButtons, Item } from 'react-navigation-header-buttons'; -import { Challenge, PrivilegeCredential, ProtectedAction } from 'snjs'; +import { + Challenge, + DeinitSource, + PrivilegeCredential, + ProtectedAction, +} from 'snjs'; import { NoteHistoryEntry } from 'snjs/dist/@types/services/history/entries/note_history_entry'; import { ThemeContext, ThemeProvider } from 'styled-components/native'; import { ApplicationContext } from './ApplicationContext'; @@ -397,8 +402,10 @@ const MainStackComponent = ({ env }: { env: 'prod' | 'dev' }) => { title={'Destroy Data'} onPress={async () => { await application?.deviceInterface?.removeAllRawStorageValues(); - await application?.deviceInterface?.removeAllRawDatabasePayloads(); - application?.deinit(); + await application?.deviceInterface?.removeAllRawDatabasePayloads( + application?.identifier + ); + application?.deinit(DeinitSource.SignOut); }} /> @@ -600,6 +607,7 @@ const AppComponent: React.FC<{ }; const AppGroupInstance = new ApplicationGroup(); +AppGroupInstance.initialize(); export const App = (props: { env: 'prod' | 'dev' }) => { const applicationGroupRef = useRef(AppGroupInstance); @@ -609,12 +617,13 @@ export const App = (props: { env: 'prod' | 'dev' }) => { useEffect(() => { const removeAppChangeObserver = applicationGroupRef.current.addApplicationChangeObserver( () => { - setApplication(applicationGroupRef.current.application); + const mobileApplication = applicationGroupRef.current + .primaryApplication as MobileApplication; + setApplication(mobileApplication); } ); - return removeAppChangeObserver; - }, [applicationGroupRef.current.application]); + }, [applicationGroupRef.current.primaryApplication]); return ( {application && ( diff --git a/src/lib/InstallationService.ts b/src/lib/InstallationService.ts index d1f11cfd..2fc4c125 100644 --- a/src/lib/InstallationService.ts +++ b/src/lib/InstallationService.ts @@ -46,7 +46,9 @@ export class InstallationService extends ApplicationService { const hasNormalKeys = this.application?.hasAccount() || this.application?.hasPasscode(); - const keychainKey = await this.application?.deviceInterface?.getNamespacedKeychainValue(); + const keychainKey = await this.application?.deviceInterface?.getNamespacedKeychainValue( + this.application?.identifier + ); const hasKeychainValue = !isNullOrUndefined(keychainKey); let firstRunKey = await this.application?.getValue( @@ -73,7 +75,9 @@ export class InstallationService extends ApplicationService { if (confirmed) { await this.application?.deviceInterface?.removeAllRawStorageValues(); - await this.application?.deviceInterface?.removeAllRawDatabasePayloads(); + await this.application?.deviceInterface?.removeAllRawDatabasePayloads( + this.application?.identifier + ); await this.application?.deviceInterface?.clearRawKeychainValue(); } else { SNReactNative.exitApp(); diff --git a/src/lib/application.ts b/src/lib/application.ts index 9b9e8218..3d442207 100644 --- a/src/lib/application.ts +++ b/src/lib/application.ts @@ -1,5 +1,4 @@ import { SCREEN_AUTHENTICATE } from '@Screens/screens'; -import { StyleKit } from '@Style/StyleKit'; import { Platform } from 'react-native'; import VersionInfo from 'react-native-version-info'; import { @@ -9,6 +8,7 @@ import { SNApplication, SNComponentManager, } from 'snjs'; +import { DeinitSource } from 'snjs/dist/@types/types'; import { AlertService } from './AlertService'; import { ApplicationState } from './ApplicationState'; import { BackupsService } from './BackupsService'; @@ -27,26 +27,23 @@ type MobileServices = { reviewService: ReviewService; backupsService: BackupsService; installationService: InstallationService; - themeService: StyleKit; prefsService: PreferencesManager; }; export class MobileApplication extends SNApplication { - private onDeinit?: (app: MobileApplication) => void; private MobileServices!: MobileServices; public editorGroup: EditorGroup; public componentGroup: ComponentGroup; public Uuid: string; // UI remounts when Uuid changes - constructor(onDeinit: (app: MobileApplication) => void) { - const deviceInterface = new MobileDeviceInterface(); + constructor(deviceInterface: MobileDeviceInterface, identifier: string) { super( Environment.Mobile, platformFromString(Platform.OS), deviceInterface, new SNReactNativeCrypto(), new AlertService(), - undefined, + identifier, [ { swap: SNComponentManager, @@ -59,12 +56,12 @@ export class MobileApplication extends SNApplication { : 'https://sync.standardnotes.org' ); this.Uuid = Math.random().toString(); - this.onDeinit = onDeinit; this.editorGroup = new EditorGroup(this); this.componentGroup = new ComponentGroup(this); } - deinit() { + /** @override */ + deinit(source: DeinitSource) { for (const key of Object.keys(this.MobileServices)) { const service = (this.MobileServices as any)[key]; if (service.deinit) { @@ -73,11 +70,9 @@ export class MobileApplication extends SNApplication { service.application = undefined; } this.MobileServices = {} as MobileServices; - this.onDeinit!(this); - this.onDeinit = undefined; this.editorGroup.deinit(); this.componentGroup.deinit(); - super.deinit(); + super.deinit(source); } promptForChallenge(challenge: Challenge) { diff --git a/src/lib/applicationGroup.ts b/src/lib/applicationGroup.ts index 6dc7ee33..39619777 100644 --- a/src/lib/applicationGroup.ts +++ b/src/lib/applicationGroup.ts @@ -1,87 +1,47 @@ -import { StyleKit } from '@Style/StyleKit'; -import { removeFromArray } from 'snjs'; +import { + ApplicationDescriptor, + DeviceInterface, + SNApplicationGroup, +} from 'snjs'; import { MobileApplication } from './application'; import { ApplicationState } from './ApplicationState'; import { BackupsService } from './BackupsService'; import { InstallationService } from './InstallationService'; +import { MobileDeviceInterface } from './interface'; import { PreferencesManager } from './PreferencesManager'; import { ReviewService } from './reviewService'; -type AppManagerChangeCallback = () => void; - -export class ApplicationGroup { - applications: MobileApplication[] = []; - changeObservers: AppManagerChangeCallback[] = []; - activeApplication?: MobileApplication; - +export class ApplicationGroup extends SNApplicationGroup { constructor() { - this.onApplicationDeinit = this.onApplicationDeinit.bind(this); - this.createDefaultApplication(); + super(new MobileDeviceInterface()); } - private async createDefaultApplication() { - this.activeApplication = this.createNewApplication(); - this.applications.push(this.activeApplication); - - this.notifyObserversOfAppChange(); + async initialize(_callback?: any) { + await super.initialize({ + applicationCreator: this.createApplication, + }); } - async onApplicationDeinit(application: MobileApplication) { - removeFromArray(this.applications, application); - if (this.activeApplication === application) { - this.activeApplication = undefined; - } - if (this.applications.length === 0) { - await this.createDefaultApplication(); - } - } - - private createNewApplication() { - const application = new MobileApplication(this.onApplicationDeinit); + private createApplication = ( + descriptor: ApplicationDescriptor, + deviceInterface: DeviceInterface + ) => { + const application = new MobileApplication( + deviceInterface as MobileDeviceInterface, + descriptor.identifier + ); const applicationState = new ApplicationState(application); const reviewService = new ReviewService(application); const backupsService = new BackupsService(application); - const themeService = new StyleKit(application); const prefsService = new PreferencesManager(application); const installationService = new InstallationService(application); application.setMobileServices({ applicationState, reviewService, backupsService, - themeService, prefsService, installationService, }); return application; - } - - get application() { - return this.activeApplication; - } - - public getApplications() { - return this.applications.slice(); - } - - /** - * Notifies observer when the active application has changed. - * Any application which is no longer active is destroyed, and - * must be removed from the interface. - */ - public addApplicationChangeObserver(callback: AppManagerChangeCallback) { - this.changeObservers.push(callback); - if (this.application) { - callback(); - } - - return () => { - removeFromArray(this.changeObservers, callback); - }; - } - - private notifyObserversOfAppChange() { - for (const observer of this.changeObservers) { - observer(); - } - } + }; } diff --git a/src/lib/interface.ts b/src/lib/interface.ts index 6cb47ab5..7ad9f40b 100644 --- a/src/lib/interface.ts +++ b/src/lib/interface.ts @@ -1,7 +1,7 @@ import AsyncStorage from '@react-native-community/async-storage'; import { Alert, Linking, Platform } from 'react-native'; import FingerprintScanner from 'react-native-fingerprint-scanner'; -import { DeviceInterface } from 'snjs'; +import { ApplicationIdentifier, DeviceInterface } from 'snjs'; import Keychain from './keychain'; export type BiometricsType = @@ -19,22 +19,21 @@ export class MobileDeviceInterface extends DeviceInterface { super.deinit(); } - private getDatabaseKeyPrefix() { - if (this.namespace!.identifier) { - return `${this.namespace!.identifier}-Item-`; - } else { - return 'Item-'; - } + private getDatabaseKeyPrefix(_identifier: ApplicationIdentifier) { + /** + * The default identifier (standardnotes) is used, but we don't want to prefix items with it just yet. + */ + return 'Item-'; } - private keyForPayloadId(id: string) { - return `${this.getDatabaseKeyPrefix()}${id}`; + private keyForPayloadId(id: string, identifier: ApplicationIdentifier) { + return `${this.getDatabaseKeyPrefix(identifier)}${id}`; } - private async getAllDatabaseKeys() { + private async getAllDatabaseKeys(identifier: ApplicationIdentifier) { const keys = await AsyncStorage.getAllKeys(); const filtered = keys.filter(key => { - return key.includes(this.getDatabaseKeyPrefix()); + return key.includes(this.getDatabaseKeyPrefix(identifier)); }); return filtered; } @@ -125,59 +124,75 @@ export class MobileDeviceInterface extends DeviceInterface { return Promise.resolve({ isNewDatabase: false }); } - async getAllRawDatabasePayloads(): Promise { - const keys = await this.getAllDatabaseKeys(); + async getAllRawDatabasePayloads( + identifier: ApplicationIdentifier + ): Promise { + const keys = await this.getAllDatabaseKeys(identifier); return this.getDatabaseKeyValues(keys); } - saveRawDatabasePayload(payload: any): Promise { - return this.saveRawDatabasePayloads([payload]); + saveRawDatabasePayload( + payload: any, + identifier: ApplicationIdentifier + ): Promise { + return this.saveRawDatabasePayloads([payload], identifier); } - async saveRawDatabasePayloads(payloads: any[]): Promise { + async saveRawDatabasePayloads( + payloads: any[], + identifier: ApplicationIdentifier + ): Promise { if (payloads.length === 0) { return; } await Promise.all( payloads.map(item => { return AsyncStorage.setItem( - this.keyForPayloadId(item.uuid), + this.keyForPayloadId(item.uuid, identifier), JSON.stringify(item) ); }) ); } - removeRawDatabasePayloadWithId(id: string): Promise { - return this.removeRawStorageValue(this.keyForPayloadId(id)); + removeRawDatabasePayloadWithId( + id: string, + identifier: ApplicationIdentifier + ): Promise { + return this.removeRawStorageValue(this.keyForPayloadId(id, identifier)); } - async removeAllRawDatabasePayloads(): Promise { - const keys = await this.getAllDatabaseKeys(); + async removeAllRawDatabasePayloads( + identifier: ApplicationIdentifier + ): Promise { + const keys = await this.getAllDatabaseKeys(identifier); return AsyncStorage.multiRemove(keys); } - async getNamespacedKeychainValue() { + async getNamespacedKeychainValue(identifier: ApplicationIdentifier) { const keychain = await this.getRawKeychainValue(); if (!keychain) { return; } - return keychain[this.namespace!.identifier]; + return keychain[identifier]; } - async setNamespacedKeychainValue(value: any) { + async setNamespacedKeychainValue( + value: any, + identifier: ApplicationIdentifier + ) { let keychain = await this.getRawKeychainValue(); if (!keychain) { keychain = {}; } return Keychain.setKeys({ ...keychain, - [this.namespace!.identifier]: value, + [identifier]: value, }); } - async clearNamespacedKeychainValue() { + async clearNamespacedKeychainValue(identifier: ApplicationIdentifier) { const keychain = await this.getRawKeychainValue(); if (!keychain) { return; } - delete keychain[this.namespace!.identifier]; + delete keychain[identifier]; return Keychain.setKeys(keychain); } diff --git a/src/screens/Settings/Sections/AuthSection.tsx b/src/screens/Settings/Sections/AuthSection.tsx index 0874a707..f01e1ad2 100644 --- a/src/screens/Settings/Sections/AuthSection.tsx +++ b/src/screens/Settings/Sections/AuthSection.tsx @@ -7,6 +7,7 @@ import { ApplicationContext } from '@Root/ApplicationContext'; import { StyleKitContext } from '@Style/StyleKit'; import React, { useCallback, useContext, useEffect, useState } from 'react'; import { Keyboard } from 'react-native'; +import { HttpResponse } from 'snjs/dist/@types/services/api/http_service'; import { RegistrationDescription, RegistrationInput, @@ -22,15 +23,6 @@ type Props = { signedIn: boolean; }; -type MfaResponse = { - message: string; - payload: { - mfa_key: string; - }; - tag: string; - status: number; -}; - export const AuthSection = (props: Props) => { // Context const application = useContext(ApplicationContext); @@ -41,7 +33,7 @@ export const AuthSection = (props: Props) => { const [signingIn, setSigningIn] = useState(false); const [strictSignIn, setStrictSignIn] = useState(false); const [showAdvanced, setShowAdvanced] = useState(false); - const [mfa, setMfa] = useState(); + const [mfa, setMfa] = useState(); const [mfaCode, setMfaCode] = useState(''); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); @@ -103,7 +95,7 @@ export const AuthSection = (props: Props) => { password, strictSignIn, undefined, - mfa?.payload.mfa_key, + mfa?.payload?.mfa_key, mfaCode || undefined, true, false @@ -114,7 +106,7 @@ export const AuthSection = (props: Props) => { result?.error.tag === 'mfa-required' || result?.error.tag === 'mfa-invalid' ) { - setMfa(result?.error); + setMfa(result.error); setMfaCode(''); } else if (result?.error.message) { application?.alertService?.alert(result?.error.message, 'Oops', 'OK'); diff --git a/yarn.lock b/yarn.lock index 3a927c78..88232bb6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7529,9 +7529,9 @@ sncrypto@standardnotes/sncrypto#5f8cd36: version "1.2.0" resolved "https://codeload.github.com/standardnotes/sncrypto/tar.gz/5f8cd369773cec7f342c23ecaa659d932b35cd31" -snjs@standardnotes/snjs#d1f61cd: +snjs@standardnotes/snjs#9003251: version "1.0.5" - resolved "https://codeload.github.com/standardnotes/snjs/tar.gz/d1f61cdd7eebe65071823ef1ba2bd95cfd9f0500" + resolved "https://codeload.github.com/standardnotes/snjs/tar.gz/9003251047822f4353bcb83ec4dca59d9aebb301" source-map-resolve@^0.5.0: version "0.5.3"