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 <johnny243@users.noreply.github.com>
Co-authored-by: Johnny A <5891646+johnny243@users.noreply.github.com>
This commit is contained in:
Radek Czemerys
2020-09-17 13:20:42 +02:00
committed by GitHub
parent 24abfc8d72
commit 545c07ecf3
8 changed files with 97 additions and 122 deletions

View File

@@ -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"
},

View File

@@ -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);
}}
/>
</HeaderButtons>
@@ -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 (
<ApplicationContext.Provider value={application}>
{application && (

View File

@@ -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();

View File

@@ -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) {

View File

@@ -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();
}
}
};
}

View File

@@ -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<unknown[]> {
const keys = await this.getAllDatabaseKeys();
async getAllRawDatabasePayloads(
identifier: ApplicationIdentifier
): Promise<unknown[]> {
const keys = await this.getAllDatabaseKeys(identifier);
return this.getDatabaseKeyValues(keys);
}
saveRawDatabasePayload(payload: any): Promise<void> {
return this.saveRawDatabasePayloads([payload]);
saveRawDatabasePayload(
payload: any,
identifier: ApplicationIdentifier
): Promise<void> {
return this.saveRawDatabasePayloads([payload], identifier);
}
async saveRawDatabasePayloads(payloads: any[]): Promise<void> {
async saveRawDatabasePayloads(
payloads: any[],
identifier: ApplicationIdentifier
): Promise<void> {
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<void> {
return this.removeRawStorageValue(this.keyForPayloadId(id));
removeRawDatabasePayloadWithId(
id: string,
identifier: ApplicationIdentifier
): Promise<void> {
return this.removeRawStorageValue(this.keyForPayloadId(id, identifier));
}
async removeAllRawDatabasePayloads(): Promise<void> {
const keys = await this.getAllDatabaseKeys();
async removeAllRawDatabasePayloads(
identifier: ApplicationIdentifier
): Promise<void> {
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);
}

View File

@@ -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<MfaResponse | undefined>();
const [mfa, setMfa] = useState<HttpResponse['error'] | undefined>();
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');

View File

@@ -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"