mirror of
https://github.com/standardnotes/mobile.git
synced 2026-05-19 12:04:31 -04:00
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:
@@ -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"
|
||||
},
|
||||
|
||||
21
src/App.tsx
21
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);
|
||||
}}
|
||||
/>
|
||||
</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 && (
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user