From 269f3e589244e4dfb87d9e92ea80dd02f69d4e90 Mon Sep 17 00:00:00 2001 From: Radek Czemerys Date: Sat, 16 May 2020 13:36:10 +0200 Subject: [PATCH] lib folder migration --- .vscode/settings.json | 3 + ios/Podfile.lock | 8 +- package.json | 14 +- src/App.tsx | 8 - src/lib/AlertService.ts | 74 ++ src/lib/ApplicationState.ts | 602 ++++++++-------- src/lib/BackupsManager.ts | 188 ----- src/lib/BackupsService.ts | 154 ++++ src/lib/Editor.ts | 100 +++ src/lib/EditorGroup.ts | 74 ++ src/lib/OptionsState.ts | 174 ----- src/lib/PreferencesManager.ts | 80 +++ src/lib/application.ts | 91 +++ src/lib/applicationGroup.ts | 85 +++ src/lib/componentGroup.ts | 100 +++ src/lib/componentManager.ts | 199 ------ src/lib/interface.ts | 126 ++++ src/lib/itemActionManager.ts | 139 ---- src/lib/keysManager.ts | 658 ------------------ src/lib/reviewManager.ts | 36 - src/lib/reviewService.ts | 35 + src/lib/snjs/alertManager.ts | 75 -- src/lib/snjs/authManager.ts | 98 --- src/lib/snjs/httpManager.ts | 23 - src/lib/snjs/migrationManager.ts | 121 ---- src/lib/snjs/modelManager.ts | 369 ---------- src/lib/snjs/privilegesManager.ts | 160 ----- src/lib/snjs/storageManager.ts | 220 ------ src/lib/snjs/syncManager.ts | 72 -- src/lib/userPrefsManager.ts | 57 -- src/screens/Abstract.tsx | 1 - src/screens/ComponentView.tsx | 6 - src/screens/KeyRecovery.tsx | 4 - src/screens/ManagePrivileges.tsx | 5 +- src/screens/Root.tsx | 5 - .../Settings/Sections/OptionsSection.tsx | 5 - src/screens/Settings/Settings.tsx | 2 +- src/screens/SideMenu/MainSideMenu.tsx | 6 +- src/style/themesManager.ts | 187 +++++ src/style/utils.ts | 4 +- src/types/snjs/index.d.ts | 37 - yarn.lock | 106 ++- 42 files changed, 1500 insertions(+), 3011 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/lib/AlertService.ts delete mode 100644 src/lib/BackupsManager.ts create mode 100644 src/lib/BackupsService.ts create mode 100644 src/lib/Editor.ts create mode 100644 src/lib/EditorGroup.ts delete mode 100644 src/lib/OptionsState.ts create mode 100644 src/lib/PreferencesManager.ts create mode 100644 src/lib/application.ts create mode 100644 src/lib/applicationGroup.ts create mode 100644 src/lib/componentGroup.ts delete mode 100644 src/lib/componentManager.ts create mode 100644 src/lib/interface.ts delete mode 100644 src/lib/itemActionManager.ts delete mode 100644 src/lib/keysManager.ts delete mode 100644 src/lib/reviewManager.ts create mode 100644 src/lib/reviewService.ts delete mode 100644 src/lib/snjs/alertManager.ts delete mode 100644 src/lib/snjs/authManager.ts delete mode 100644 src/lib/snjs/httpManager.ts delete mode 100644 src/lib/snjs/migrationManager.ts delete mode 100644 src/lib/snjs/modelManager.ts delete mode 100644 src/lib/snjs/privilegesManager.ts delete mode 100644 src/lib/snjs/storageManager.ts delete mode 100644 src/lib/snjs/syncManager.ts delete mode 100644 src/lib/userPrefsManager.ts create mode 100644 src/style/themesManager.ts delete mode 100644 src/types/snjs/index.d.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..55712c19 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib" +} \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 6303a9ef..4305ea58 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -311,7 +311,7 @@ PODS: - React - ReactNativeDarkMode (0.2.2): - React - - RNCAsyncStorage (1.6.3): + - RNCAsyncStorage (1.10.1): - React - RNCMaskedView (0.1.10): - React @@ -321,7 +321,7 @@ PODS: - React - RNGestureHandler (1.6.1): - React - - RNKeychain (4.0.5): + - RNKeychain (6.0.0): - React - RNReanimated (1.8.0): - React @@ -548,12 +548,12 @@ SPEC CHECKSUMS: ReactCommon: ed4e11d27609d571e7eee8b65548efc191116eb3 ReactNativeAlternateIcons: b2a8a729d9d9756ed0652c352694f190407f297f ReactNativeDarkMode: 0178ffca3b10f6a7c9f49d6f9810232b328fa949 - RNCAsyncStorage: 3c304d1adfaea02ec732ac218801cb13897aa8c0 + RNCAsyncStorage: 39831e0dc5f547a1fb2669eb9e091d460a9be2be RNCMaskedView: 5a8ec07677aa885546a0d98da336457e2bea557f RNFileViewer: b815b353fdc08552766c6325e5b66ff52bb6b7af RNFS: 2bd9eb49dc82fa9676382f0585b992c424cd59df RNGestureHandler: 8f09cd560f8d533eb36da5a6c5a843af9f056b38 - RNKeychain: 840f8e6f13be0576202aefcdffd26a4f54bfe7b5 + RNKeychain: bf2d7e9a0ae7a073c07770dd2aa6d11c67581733 RNReanimated: 955cf4068714003d2f1a6e2bae3fb1118f359aff RNScreens: cf198f915f8a2bf163de94ca9f5bfc8d326c3706 RNStoreReview: 62d6afd7c37db711a594bbffca6b0ea3a812b7a8 diff --git a/package.json b/package.json index 5dc40485..6a38bdb0 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "postinstall": "patch-package" }, "dependencies": { - "@react-native-community/async-storage": "1.6.3", + "@react-native-community/async-storage": "1.10.1", "@react-native-community/masked-view": "^0.1.10", "base-64": "^0.1.0", "bugsnag-react-native": "^2.23.7", @@ -39,7 +39,7 @@ "react-native-flag-secure-android": "standardnotes/react-native-flag-secure-android#3d59055", "react-native-fs": "^2.16.6", "react-native-gesture-handler": "^1.6.1", - "react-native-keychain": "^4.0.1", + "react-native-keychain": "^6.0.0", "react-native-mail": "standardnotes/react-native-mail#9862c76", "react-native-reanimated": "^1.8.0", "react-native-safe-area-context": "^1.0.0", @@ -54,7 +54,7 @@ "react-navigation-stack": "^1.10.3", "regenerator": "^0.14.2", "sn-textview": "standardnotes/sn-textview#f42f0bf", - "snjs": "standardnotes/snjs#9382050", + "snjs": "standardnotes/snjs#a18da1b", "standard-notes-rn": "standardnotes/standard-notes-rn" }, "devDependencies": { @@ -65,10 +65,10 @@ "@types/faker": "^4.1.11", "@types/jest": "^25.2.1", "@types/lodash": "^4.14.150", - "@types/react-native": "^0.62.7", + "@types/react-native": "^0.62.8", "@types/react-native-vector-icons": "^6.4.5", - "@typescript-eslint/eslint-plugin": "^2.31.0", - "@typescript-eslint/parser": "^2.31.0", + "@typescript-eslint/eslint-plugin": "^2.33.0", + "@typescript-eslint/parser": "^2.33.0", "babel-jest": "^26.0.1", "concurrently": "^5.2.0", "detox": "^16.5.0", @@ -83,7 +83,7 @@ "prettier": "^2.0.5", "react-test-renderer": "16.11.0", "replace-in-file": "^6.0.0", - "typescript": "^3.8.3" + "typescript": "^3.9.2" }, "jest": { "preset": "react-native" diff --git a/src/App.tsx b/src/App.tsx index b868b379..243882fc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,14 +9,6 @@ import { import { createAppContainer, NavigationActions } from 'react-navigation'; import { createDrawerNavigator, DrawerActions } from 'react-navigation-drawer'; import { createStackNavigator } from 'react-navigation-stack'; -import KeysManager from '@Lib/keysManager'; -import ApplicationState from '@Lib/ApplicationState'; -import Auth from '@Lib/snjs/authManager'; -import ModelManager from '@Lib/snjs/modelManager'; -import PrivilegesManager from '@Lib/snjs/privilegesManager'; -import MigrationManager from '@Lib/snjs/migrationManager'; -import Sync from '@Lib/snjs/syncManager'; -import ReviewManager from '@Lib/reviewManager'; import Authenticate from '@Screens/Authentication/Authenticate'; import Compose from '@Screens/Compose'; import { diff --git a/src/lib/AlertService.ts b/src/lib/AlertService.ts new file mode 100644 index 00000000..b3db8158 --- /dev/null +++ b/src/lib/AlertService.ts @@ -0,0 +1,74 @@ +import { Alert } from 'react-native'; + +import { SNAlertService } from 'snjs'; + +export class AlertService extends SNAlertService { + async alert( + title: string, + text: string, + closeButtonText?: string, + onClose?: () => void + ) { + return new Promise(resolve => { + // On iOS, confirm should go first. On Android, cancel should go first. + let buttons = [ + { + text: closeButtonText, + onPress: async () => { + if (onClose) { + onClose(); + } + resolve(); + }, + }, + ]; + Alert.alert(title, text, buttons, { + cancelable: true, + }); + }); + } + + async confirm( + text: string, + title: string, + confirmButtonText = 'Confirm', + cancelButtonText = 'Cancel', + onConfirm: () => void, + onCancel: () => void, + _destructive = false + ) { + return new Promise((resolve, reject) => { + // On iOS, confirm should go first. On Android, cancel should go first. + let buttons = [ + { + text: cancelButtonText, + onPress: async () => { + if (onCancel) { + onCancel(); + } + reject(); + }, + }, + { + text: confirmButtonText, + onPress: async () => { + if (onConfirm) { + onConfirm(); + } + resolve(); + }, + }, + ]; + Alert.alert(title, text, buttons, { + cancelable: true, + onDismiss: async () => { + // TODO: check alerts + // if (onDismiss) { + // onDismiss(); + // } + reject(); + }, + }); + }); + } +} diff --git a/src/lib/ApplicationState.ts b/src/lib/ApplicationState.ts index 91592d88..979e59af 100644 --- a/src/lib/ApplicationState.ts +++ b/src/lib/ApplicationState.ts @@ -9,30 +9,42 @@ import { KeyboardEventListener, EmitterSubscription, } from 'react-native'; -import _ from 'lodash'; -import KeysManager from '@Lib/keysManager'; -import OptionsState from '@Lib/OptionsState'; -import PrivilegesManager from '@Lib/snjs/privilegesManager'; -import AuthenticationSourceLocalPasscode from '@Screens/Authentication/Sources/AuthenticationSourceLocalPasscode'; -import AuthenticationSourceBiometric from '@Screens/Authentication/Sources/AuthenticationSourceBiometric'; +import { pull } from 'lodash'; +import { MobileApplication } from './application'; +import { Editor } from './editor'; +import { + SNNote, + ContentType, + PayloadSource, + SNUserPrefs, + SNTag, + ApplicationEvent, + SNSmartTag, +} from 'snjs'; const pjson = require('../../package.json'); const { PlatformConstants } = NativeModules; -export type AppStateType = - | typeof ApplicationState.Launching - | typeof ApplicationState.LosingFocus - | typeof ApplicationState.Backgrounding - | typeof ApplicationState.GainingFocus - | typeof ApplicationState.ResumingFromBackground - | typeof ApplicationState.Locking - | typeof ApplicationState.Unlocking; +export enum AppStateType { + Launching = 1, + LosingFocus = 2, + EnteringBackground = 3, + GainingFocus = 4, + EditorFocused = 5, + ResumingFromBackground = 6, + Locking = 7, + Unlocking = 8, + TagChanged = 9, + ActiveEditorChanged = 10, + PreferencesChanged = 11, +} + +export enum AppStateEventType { + KeyboardChangeEvent = 1, + AppStateEventTabletModeChange = 2, + AppStateEventNoteSideMenuToggle = 3, +} -// AppStateEvents -export type AppStateEventType = - | typeof ApplicationState.KeyboardChangeEvent - | typeof ApplicationState.AppStateEventTabletModeChange - | typeof ApplicationState.AppStateEventNoteSideMenuToggle; export type TabletModeChangeData = { new_isInTabletMode: boolean; old_isInTabletMode: boolean; @@ -42,91 +54,36 @@ export type NoteSideMenuToggleChange = { old_isNoteSideMenuCollapsed: boolean; }; -type KeyboardChangeEventHandler = ( - event: typeof ApplicationState.KeyboardChangeEvent, - data: undefined -) => void; -type SideMenuToogleEvent = ( - event: typeof ApplicationState.AppStateEventNoteSideMenuToggle, - data: NoteSideMenuToggleChange -) => void; -type TableModeChageEvent = ( - event: typeof ApplicationState.AppStateEventTabletModeChange, - data: TabletModeChangeData -) => void; -export type AppStateEventHandler = - | KeyboardChangeEventHandler - | SideMenuToogleEvent - | TableModeChageEvent; +type EventObserverCallback = ( + event: AppStateEventType, + data?: TabletModeChangeData | NoteSideMenuToggleChange +) => Promise; +type ObserverCallback = (event: AppStateType, data?: any) => Promise; -type Observer = { - key: () => number; - callback: (state: AppStateType) => void; -}; - -export default class ApplicationState { - // When the app first launches - static Launching = 'Launching' as 'Launching'; - - // When the app enters into multitasking view, or control/notification center for iOS - static LosingFocus = 'LosingFocus' as 'LosingFocus'; - - // When the app enters the background completely - static Backgrounding = 'Backgrounding' as 'Backgrounding'; - - // When the app resumes from either the background or from multitasking switcher or notification center - static GainingFocus = 'GainingFocus' as 'GainingFocus'; - - // When the app resumes from the background - static ResumingFromBackground = 'ResumingFromBackground' as 'ResumingFromBackground'; - - // When the user enters their local passcode and/or fingerprint - static Locking = 'Locking' as 'Locking'; - - // When the user enters their local passcode and/or fingerprint - static Unlocking = 'Unlocking' as 'Unlocking'; - - /* Seperate events, unrelated to app state notifications */ - static AppStateEventTabletModeChange = 'AppStateEventTabletModeChange' as 'AppStateEventTabletModeChange'; - static AppStateEventNoteSideMenuToggle = 'AppStateEventNoteSideMenuToggle' as 'AppStateEventNoteSideMenuToggle'; - static KeyboardChangeEvent = 'KeyboardChangeEvent' as 'KeyboardChangeEvent'; - - private static instance: ApplicationState; - _isAndroid: boolean; - observers: Observer[]; - eventSubscribers: AppStateEventHandler[]; - locked: boolean; - keyboardDidShowListener: EmitterSubscription; - keyboardDidHideListener: EmitterSubscription; +export class ApplicationState { + application: MobileApplication; + observers: ObserverCallback[] = []; + eventObservers: EventObserverCallback[] = []; + locked = true; + keyboardDidShowListener?: EmitterSubscription; + keyboardDidHideListener?: EmitterSubscription; keyboardHeight?: number; - optionsState: OptionsState; - loading: boolean = false; + appEventObersever: any; + selectedTag?: SNTag; + userPreferences?: SNUserPrefs; tabletMode: boolean = false; noteSideMenuCollapsed: boolean = false; ignoreStateChanges: boolean = false; mostRecentState?: AppStateType; - didHandleApplicationStart: boolean = false; authenticationInProgress: boolean = false; - static get() { - if (!this.instance) { - this.instance = new ApplicationState(); - } - - return this.instance; - } - - constructor() { - this.observers = []; - this.optionsState = new OptionsState(); - this.eventSubscribers = []; - this.locked = true; - this._isAndroid = Platform.OS === 'android'; + multiEditorEnabled = false; + constructor(application: MobileApplication) { + this.application = application; this.setTabletModeEnabled(this.isTabletDevice); - this.initializeOptions(); - - AppState.addEventListener('change', this.handleAppStateChange); - this.didLaunch(); + this.handleApplicationEvents(); + this.handleItemsChanges(); + AppState.addEventListener('change', this.handleReactNativeAppStateChange); this.keyboardDidShowListener = Keyboard.addListener( 'keyboardWillShow', @@ -138,57 +95,226 @@ export default class ApplicationState { ); } - keyboardDidShow: KeyboardEventListener = e => { + deinit() { + this.appEventObersever(); + this.appEventObersever = undefined; + this.observers.length = 0; + this.keyboardDidShowListener = undefined; + this.keyboardDidHideListener = undefined; + } + + /** + * Registers an observer for App State change + * @returns function that unregisters this observer + */ + public addStateChangeObserver(callback: ObserverCallback) { + this.observers.push(callback); + return () => { + pull(this.observers, callback); + }; + } + + /** + * Registers an observer for App State Event change + * @returns function that unregisters this observer + */ + public addStateEventObserver(callback: EventObserverCallback) { + this.eventObservers.push(callback); + return () => { + pull(this.eventObservers, callback); + }; + } + + /** + * Notify observers of ApplicationState change + */ + private notifyOfStateChange(state: AppStateType, data?: any) { + if (this.ignoreStateChanges) { + return; + } + + // Set most recent state before notifying observers, in case they need to query this value. + this.mostRecentState = state; + + for (const observer of this.observers) { + observer(state, data); + } + } + + /** + * Notify observers of ApplicationState Events + */ + private notifyEventObservers( + event: AppStateEventType, + data?: TabletModeChangeData | NoteSideMenuToggleChange + ) { + for (const observer of this.eventObservers) { + observer(event, data); + } + } + + /** + * Creates a new editor if one doesn't exist. If one does, we'll replace the + * editor's note with an empty one. + */ + createEditor(title?: string) { + const activeEditor = this.getActiveEditor(); + if (!activeEditor || this.multiEditorEnabled) { + this.application.editorGroup.createEditor(undefined, title); + } else { + activeEditor.reset(title); + } + } + + getActiveEditor() { + return this.application.editorGroup.editors[0]; + } + + getEditors() { + return this.application.editorGroup.editors; + } + + closeEditor(editor: Editor) { + this.application.editorGroup.closeEditor(editor); + } + + closeActiveEditor() { + this.application.editorGroup.closeActiveEditor(); + } + + closeAllEditors() { + this.application.editorGroup.closeAllEditors(); + } + + editorForNote(note: SNNote) { + for (const editor of this.getEditors()) { + if (editor.note.uuid === note.uuid) { + return editor; + } + } + } + + private keyboardDidShow: KeyboardEventListener = e => { this.keyboardHeight = e.endCoordinates.height; - this.notifyEvent(ApplicationState.KeyboardChangeEvent); + this.notifyEventObservers(AppStateEventType.KeyboardChangeEvent); }; - keyboardDidHide: KeyboardEventListener = () => { + private keyboardDidHide: KeyboardEventListener = () => { this.keyboardHeight = 0; - this.notifyEvent(ApplicationState.KeyboardChangeEvent); + this.notifyEventObservers(AppStateEventType.KeyboardChangeEvent); }; + /** + * @returns Returns keybord height + */ getKeyboardHeight() { return this.keyboardHeight; } - initializeOptions() { - // Initialize Options (sort by, filter, selected tags, etc) - this.optionsState.addChangeObserver(options => { - if (!this.loading) { - options.persist(); + /** + * Reacts to @SNNote and @SNTag Changes + */ + private handleItemsChanges() { + this.application!.streamItems( + [ContentType.Note, ContentType.Tag], + async (items, source) => { + /** Close any editors for deleted/trashed/archived notes */ + if (source === PayloadSource.PreSyncSave) { + const notes = items.filter( + candidate => candidate.content_type === ContentType.Note + ) as SNNote[]; + for (const note of notes) { + const editor = this.editorForNote(note); + if (!editor) { + continue; + } + if (note.deleted) { + this.closeEditor(editor); + } else if (note.trashed && !this.selectedTag?.isTrashTag) { + this.closeEditor(editor); + } else if (note.archived && !this.selectedTag?.isArchiveTag) { + this.closeEditor(editor); + } + } + } + if (this.selectedTag) { + const matchingTag = items.find( + candidate => candidate.uuid === this.selectedTag!.uuid + ); + if (matchingTag) { + this.selectedTag = matchingTag as SNTag; + } + } } + ); + } + + /** + * Registers for MobileApplication events + */ + private handleApplicationEvents() { + this.appEventObersever = this.application.addEventObserver( + async eventName => { + if (eventName === ApplicationEvent.Started) { + this.locked = true; + } else if (eventName === ApplicationEvent.Launched) { + this.locked = false; + } + } + ); + } + + /** + * Set selected @SNTag + */ + setSelectedTag(tag: SNTag) { + if (this.selectedTag === tag) { + return; + } + const previousTag = this.selectedTag; + this.selectedTag = tag; + this.notifyOfStateChange(AppStateType.TagChanged, { + tag: tag, + previousTag: previousTag, }); - - this.optionsState.loadSaved(); } - getOptions() { - return this.optionsState; + /** + * @returns tags that are referencing note + */ + public getNoteTags(note: SNNote) { + return this.application.referencingForItem(note).filter(ref => { + return ref.content_type === ContentType.Tag; + }) as SNTag[]; } - static getOptions() { - return this.get().getOptions(); + /** + * @returns notes this tag references + */ + public getTagNotes(tag: SNTag) { + if (tag.isSmartTag()) { + return this.application.notesMatchingSmartTag(tag as SNSmartTag); + } else { + return this.application.referencesForItem(tag).filter(ref => { + return ref.content_type === ContentType.Note; + }) as SNNote[]; + } } - static get isAndroid() { - return this.get().isAndroid; + public getSelectedTag() { + return this.selectedTag; } - static get isIOS() { - return this.get().isIOS; + setUserPreferences(preferences: SNUserPrefs) { + this.userPreferences = preferences; + this.notifyOfStateChange(AppStateType.PreferencesChanged); } static get version() { - return this.isAndroid ? pjson.versionAndroid : pjson.versionIOS; - } - - get isAndroid() { - return this._isAndroid; - } - - get isIOS() { - return !this._isAndroid; + return Platform.select({ + ios: pjson.versionIOS, + android: pjson.versionAndroid, + }); } get isTabletDevice() { @@ -200,13 +326,16 @@ export default class ApplicationState { return this.tabletMode; } - setTabletModeEnabled(enabled: boolean) { + private setTabletModeEnabled(enabled: boolean) { if (enabled !== this.tabletMode) { this.tabletMode = enabled; - this.notifyEvent(ApplicationState.AppStateEventTabletModeChange, { - new_isInTabletMode: enabled, - old_isInTabletMode: !enabled, - }); + this.notifyEventObservers( + AppStateEventType.AppStateEventTabletModeChange, + { + new_isInTabletMode: enabled, + old_isInTabletMode: !enabled, + } + ); } } @@ -217,33 +346,20 @@ export default class ApplicationState { setNoteSideMenuCollapsed(collapsed: boolean) { if (collapsed !== this.noteSideMenuCollapsed) { this.noteSideMenuCollapsed = collapsed; - this.notifyEvent(ApplicationState.AppStateEventNoteSideMenuToggle, { - new_isNoteSideMenuCollapsed: collapsed, - old_isNoteSideMenuCollapsed: !collapsed, - }); + this.notifyEventObservers( + AppStateEventType.AppStateEventNoteSideMenuToggle, + { + new_isNoteSideMenuCollapsed: collapsed, + old_isNoteSideMenuCollapsed: !collapsed, + } + ); } } - addEventHandler(handler: AppStateEventHandler) { - this.eventSubscribers.push(handler); - return handler; - } - - removeEventHandler(handler: AppStateEventHandler) { - _.pull(this.eventSubscribers, handler); - } - - notifyEvent( - event: AppStateEventType, - data?: TabletModeChangeData | NoteSideMenuToggleChange - ) { - for (const handler of this.eventSubscribers) { - // @ts-ignore not working type - handler(event, data); - } - } - - handleAppStateChange = (nextAppState: AppStateStatus) => { + /** + * handles App State change from React Native + */ + private handleReactNativeAppStateChange = (nextAppState: AppStateStatus) => { if (this.ignoreStateChanges) { return; } @@ -253,40 +369,42 @@ export default class ApplicationState { // 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 === ApplicationState.Backgrounding; + isResuming && this.mostRecentState === AppStateType.EnteringBackground; const isEnteringBackground = nextAppState === 'background'; const isLosingFocus = nextAppState === 'inactive'; if (isEnteringBackground) { - this.notifyOfState(ApplicationState.Backgrounding); + this.notifyOfStateChange(AppStateType.EnteringBackground); - if (this.shouldLockApplication()) { - this.lockApplication(); + if (true) { + // TODO: add lockManager + this.application.lock(); } } if (isResumingFromBackground || isResuming) { if (isResumingFromBackground) { - this.notifyOfState(ApplicationState.ResumingFromBackground); + this.notifyOfStateChange(AppStateType.ResumingFromBackground); } // Notify of GainingFocus even if resuming from background - this.notifyOfState(ApplicationState.GainingFocus); + this.notifyOfStateChange(AppStateType.GainingFocus); } if (isLosingFocus) { - this.notifyOfState(ApplicationState.LosingFocus); + this.notifyOfStateChange(AppStateType.LosingFocus); // If a privileges authentication session is in progress, we don't want to lock the application // or return any sources. That's because while authenticating, Face ID prompts may trigger losing focus // notifications, causing the app to lock. If the user backgrouds the app during privilege authentication, // it will still be locked via the Backgrounding event. - if ( - this.shouldLockApplication() && - !PrivilegesManager.get().authenticationInProgress() - ) { - this.lockApplication(); - } + // TODO: check this + // if ( + // this.shouldLockApplication() && + // !PrivilegesManager.get().authenticationInProgress() + // ) { + // this.lockApplication(); + // } } /* @@ -304,49 +422,14 @@ export default class ApplicationState { isAppVisibilityChange(state: AppStateType) { return ([ - ApplicationState.Launching, - ApplicationState.LosingFocus, - ApplicationState.Backgrounding, - ApplicationState.GainingFocus, - ApplicationState.ResumingFromBackground, + AppStateType.Launching, + AppStateType.LosingFocus, + AppStateType.EnteringBackground, + AppStateType.GainingFocus, + AppStateType.ResumingFromBackground, ] as Array).includes(state); } - /* State Changes */ - - // Sent from App.tsx - receiveApplicationStartEvent() { - if (this.didHandleApplicationStart) { - return; - } - this.didHandleApplicationStart = true; - var authProps = this.getAuthenticationPropsForAppState( - ApplicationState.Launching - ); - if (authProps.sources.length === 0) { - this.unlockApplication(); - } - } - - didLaunch() { - this.notifyOfState(ApplicationState.Launching); - } - - notifyOfState(state: AppStateType) { - if (this.ignoreStateChanges) { - return; - } - - // Set most recent state before notifying observers, in case they need to query this value. - this.mostRecentState = state; - - for (var observer of this.observers) { - observer.callback(state); - } - } - - /* End State */ - /* Allows other parts of the code to perform external actions without triggering state change notifications. This is useful on Android when you present a share sheet and dont want immediate authentication to appear. @@ -363,57 +446,6 @@ export default class ApplicationState { return this.mostRecentState; } - addStateObserver(callback: Observer['callback']) { - const observer = { key: Math.random, callback }; - this.observers.push(observer); - - if (this.mostRecentState) { - callback(this.mostRecentState); - } - - return observer; - } - - // clearPreviousState() { - // this.previousEvents = []; - // } - - removeStateObserver(observer: Observer) { - _.pull(this.observers, observer); - } - - /* Locking / Unlocking */ - - isLocked() { - return this.locked; - } - - isUnlocked() { - return !this.locked; - } - - shouldLockApplication() { - const showPasscode = - KeysManager.get().hasOfflinePasscode() && - KeysManager.get().passcodeTiming === 'immediately'; - const showBiometrics = - KeysManager.get().hasBiometrics() && - KeysManager.get().biometricPrefs.timing === 'immediately'; - return showPasscode || showBiometrics; - } - - lockApplication() { - this.notifyOfState(ApplicationState.Locking); - this.locked = true; - } - - unlockApplication() { - this.notifyOfState(ApplicationState.Unlocking); - this.setAuthenticationInProgress(false); - KeysManager.get().updateScreenshotPrivacy(); - this.locked = false; - } - setAuthenticationInProgress(inProgress: boolean) { this.authenticationInProgress = inProgress; } @@ -422,64 +454,6 @@ export default class ApplicationState { return this.authenticationInProgress; } - getAuthenticationPropsForAppState(state: AppStateType) { - // We don't want to do anything on gaining focus, since that may be called extraenously, - // when you come back from notification center, etc. Any immediate locking should be handled - // LosingFocus anyway. - if ( - !this.isAppVisibilityChange(state) || - state === ApplicationState.GainingFocus - ) { - return { sources: [] }; - } - - // If a privileges authentication session is in progress, we don't want to lock the application - // or return any sources. That's because while authenticating, Face ID prompts may trigger losing focus - // notifications, causing the app to lock. - if (PrivilegesManager.get().authenticationInProgress()) { - return { sources: [] }; - } - - const hasPasscode = KeysManager.get().hasOfflinePasscode(); - const hasBiometrics = KeysManager.get().hasBiometrics(); - - let showPasscode = hasPasscode, - showBiometrics = hasBiometrics; - - if ( - state === ApplicationState.Backgrounding || - state === ApplicationState.ResumingFromBackground || - state === ApplicationState.LosingFocus - ) { - showPasscode = - hasPasscode && KeysManager.get().passcodeTiming === 'immediately'; - showBiometrics = - hasBiometrics && - KeysManager.get().biometricPrefs.timing === 'immediately'; - } - - const title = - showPasscode && showBiometrics - ? 'Authentication Required' - : showPasscode - ? 'Passcode Required' - : 'Fingerprint Required'; - - let sources = []; - if (showPasscode) { - sources.push(new AuthenticationSourceLocalPasscode()); - } - if (showBiometrics) { - sources.push(new AuthenticationSourceBiometric()); - } - - return { - title: title, - sources: sources, - onAuthenticate: this.unlockApplication.bind(this), - }; - } - static openURL(url: string) { const showAlert = () => { Alert.alert('Unable to Open', `Unable to open URL ${url}.`); diff --git a/src/lib/BackupsManager.ts b/src/lib/BackupsManager.ts deleted file mode 100644 index 78b03950..00000000 --- a/src/lib/BackupsManager.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { Share, Alert } from 'react-native'; -import RNFS from 'react-native-fs'; -import FileViewer from 'react-native-file-viewer'; -import { SFItemParams } from 'snjs'; -import ApplicationState from '@Lib/ApplicationState'; -import KeysManager from '@Lib/keysManager'; -import AlertManager from '@Lib/snjs/alertManager'; -import Auth from '@Lib/snjs/authManager'; -import ModelManager from '@Lib/snjs/modelManager'; - -const Mailer = 'react-native-mail'; -const base64 = require('base-64'); - -export default class BackupsManager { - private static instance: BackupsManager; - static get() { - if (!this.instance) { - this.instance = new BackupsManager(); - } - return this.instance; - } - - /* - On iOS, we can use Share to share a file of arbitrary length. - This doesn't work on Android however. Seems to have a very low limit. - For Android, we'll use RNFS to save the file to disk, then FileViewer to - ask the user what application they would like to open the file with. - For .txt files, not many applications handle it. So, we'll want to notify the user - the path the file was saved to. - */ - - async export(encrypted: boolean) { - const auth_params = await Auth.get().getAuthParams(); - const keys = encrypted ? KeysManager.get().activeKeys() : null; - - const items = []; - - for (const item of ModelManager.get().allItems) { - const itemParams = new SFItemParams(item, keys, auth_params); - const params = await itemParams.paramsForExportFile(); - items.push(params); - } - - if (items.length === 0) { - Alert.alert('No Data', "You don't have any notes yet."); - return false; - } - - const data: { items: any; auth_params?: any } = { items }; - - if (keys) { - const authParams = KeysManager.get().activeAuthParams(); - // auth params are only needed when encrypted with a standard file key - data.auth_params = authParams; - } - - const jsonString = JSON.stringify(data, null, 2 /* pretty print */); - const modifier = encrypted ? 'Encrypted' : 'Decrypted'; - const filename = `Standard Notes ${modifier} Backup - ${this._formattedDate()}.txt`; - - if (ApplicationState.isIOS) { - return this._exportIOS(filename, jsonString); - } else { - return this._showAndroidEmailOrSaveOption().then(async result => { - if (result === 'email') { - return this._exportViaEmailAndroid(data, filename); - } else { - let filepath = await this._exportAndroid(filename, jsonString); - return this._showFileSavePromptAndroid(filepath); - } - }); - } - } - - async _showAndroidEmailOrSaveOption() { - return AlertManager.get() - .confirm({ - title: 'Choose Export Method', - cancelButtonText: 'Email', - confirmButtonText: 'Save to Disk', - }) - .then(() => { - return 'save'; - }) - .catch(() => { - return 'email'; - }); - } - - async _exportIOS(filename: string, data: string) { - return new Promise(resolve => { - ApplicationState.get().performActionWithoutStateChangeImpact(async () => { - Share.share({ - title: filename, - message: data, - }) - .then(result => { - resolve(result.action !== Share.dismissedAction); - }) - .catch(() => { - resolve(false); - }); - }); - }); - } - - async _exportAndroid(filename: string, data: string) { - const filepath = `${RNFS.ExternalDirectoryPath}/${filename}`; - return RNFS.writeFile(filepath, data).then(() => { - return filepath; - }); - } - - async _openFileAndroid(filepath: string) { - return FileViewer.open(filepath) - .then(() => { - // success - return true; - }) - .catch(error => { - console.log('Error opening file', error); - return false; - }); - } - - async _showFileSavePromptAndroid(filepath: string) { - return AlertManager.get() - .confirm({ - title: 'Backup Saved', - text: `Your backup file has been saved to your local disk at this location:\n\n${filepath}`, - cancelButtonText: 'Done', - confirmButtonText: 'Open File', - onConfirm: () => { - this._openFileAndroid(filepath); - }, - }) - .then(() => { - return true; - }) - .catch(() => { - // Did Cancel, still success - return true; - }); - } - - async _exportViaEmailAndroid(data: { items: any[] }, filename: string) { - return new Promise(resolve => { - const jsonString = JSON.stringify(data, null, 2 /* pretty print */); - const stringData = base64.encode( - unescape(encodeURIComponent(jsonString)) - ); - const fileType = '.json'; // Android creates a tmp file and expects dot with extension - - let resolved = false; - // TODO: fix mail types - // @ts-ignore - Mailer.mail( - { - subject: 'Standard Notes Backup', - recipients: [''], - body: '', - isHTML: true, - attachment: { data: stringData, type: fileType, name: filename }, - }, - (error: any) => { - if (error) { - Alert.alert('Error', 'Unable to send email.'); - } - resolved = true; - resolve(); - } - ); - - // On Android the Mailer callback event isn't always triggered. - setTimeout(function () { - if (!resolved) { - resolve(); - } - }, 2500); - }); - } - - /* Utils */ - - _formattedDate() { - return new Date().getTime(); - } -} diff --git a/src/lib/BackupsService.ts b/src/lib/BackupsService.ts new file mode 100644 index 00000000..5a82ed3e --- /dev/null +++ b/src/lib/BackupsService.ts @@ -0,0 +1,154 @@ +import { Share, Alert } from 'react-native'; +import RNFS from 'react-native-fs'; +import FileViewer from 'react-native-file-viewer'; +import { ApplicationService, Platform } from 'snjs'; +import { MobileApplication } from './application'; + +const Mailer = 'react-native-mail'; + +export class BackupsService extends ApplicationService { + /* + On iOS, we can use Share to share a file of arbitrary length. + This doesn't work on Android however. Seems to have a very low limit. + For Android, we'll use RNFS to save the file to disk, then FileViewer to + ask the user what application they would like to open the file with. + For .txt files, not many applications handle it. So, we'll want to notify the user + the path the file was saved to. + */ + + async export(encrypted: boolean) { + const data = await this.application!.createBackupFile( + undefined, + undefined, + true + ); + + const jsonString = JSON.stringify(data, null, 2 /* pretty print */); + const modifier = encrypted ? 'Encrypted' : 'Decrypted'; + const filename = `Standard Notes ${modifier} Backup - ${this._formattedDate()}.txt`; + if (data) { + if (this.application!.platform === Platform.Ios) { + return this._exportIOS(filename, jsonString); + } else { + return this._showAndroidEmailOrSaveOption().then(async result => { + if (result === 'email') { + return this._exportViaEmailAndroid(data, filename); + } else { + let filepath = await this._exportAndroid(filename, jsonString); + return this._showFileSavePromptAndroid(filepath); + } + }); + } + } + } + + async _showAndroidEmailOrSaveOption() { + return this.application!.alertService?.confirm( + 'Choose Export Method', + 'Email', + 'Save to Disk' + ) + .then(() => { + return 'save'; + }) + .catch(() => { + return 'email'; + }); + } + + async _exportIOS(filename: string, data: string) { + return new Promise(resolve => { + (this.application! as MobileApplication) + .getAppState() + .performActionWithoutStateChangeImpact(async () => { + Share.share({ + title: filename, + message: data, + }) + .then(result => { + resolve(result.action !== Share.dismissedAction); + }) + .catch(() => { + resolve(false); + }); + }); + }); + } + + async _exportAndroid(filename: string, data: string) { + const filepath = `${RNFS.ExternalDirectoryPath}/${filename}`; + return RNFS.writeFile(filepath, data).then(() => { + return filepath; + }); + } + + async _openFileAndroid(filepath: string) { + return FileViewer.open(filepath) + .then(() => { + // success + return true; + }) + .catch(error => { + console.log('Error opening file', error); + return false; + }); + } + + async _showFileSavePromptAndroid(filepath: string) { + return this.application!.alertService?.confirm( + 'Backup Saved', + `Your backup file has been saved to your local disk at this location:\n\n${filepath}`, + 'Done', + 'Open File', + () => { + this._openFileAndroid(filepath); + } + ) + .then(() => { + return true; + }) + .catch(() => { + // Did Cancel, still success + return true; + }); + } + + async _exportViaEmailAndroid(data: string, filename: string) { + return new Promise(resolve => { + const fileType = '.json'; // Android creates a tmp file and expects dot with extension + + let resolved = false; + // TODO: fix mail types + // @ts-ignore + Mailer.mail( + { + subject: 'Standard Notes Backup', + recipients: [''], + body: '', + isHTML: true, + attachment: { data, type: fileType, name: filename }, + }, + (error: any) => { + if (error) { + Alert.alert('Error', 'Unable to send email.'); + } + resolved = true; + resolve(); + } + ); + + // On Android the Mailer callback event isn't always triggered. + setTimeout(function () { + if (!resolved) { + resolve(); + } + }, 2500); + }); + } + + /* Utils */ + + _formattedDate() { + return new Date().getTime(); + } +} diff --git a/src/lib/Editor.ts b/src/lib/Editor.ts new file mode 100644 index 00000000..b239718a --- /dev/null +++ b/src/lib/Editor.ts @@ -0,0 +1,100 @@ +import { SNNote, ContentType, PayloadSource } from 'snjs'; +import { MobileApplication } from './application'; + +export class Editor { + public note!: SNNote; + private application: MobileApplication; + private _onNoteChange?: () => void; + private _onNoteValueChange?: (note: SNNote, source?: PayloadSource) => void; + private removeStreamObserver: () => void; + public isTemplateNote = false; + + constructor( + application: MobileApplication, + noteUuid?: string, + noteTitle?: string + ) { + this.application = application; + if (noteUuid) { + this.note = application.findItem(noteUuid) as SNNote; + } else { + this.reset(noteTitle); + } + + this.removeStreamObserver = this.application.streamItems( + ContentType.Note, + async (items, source) => { + await this.handleNoteStream(items as SNNote[], source); + } + ); + } + + deinit() { + this.removeStreamObserver(); + (this.removeStreamObserver as any) = undefined; + this._onNoteChange = undefined; + (this.application as any) = undefined; + this._onNoteChange = undefined; + this._onNoteValueChange = undefined; + } + + private async handleNoteStream(notes: SNNote[], source?: PayloadSource) { + /** Update our note object reference whenever it changes */ + const matchingNote = notes.find(item => { + return item.uuid === this.note.uuid; + }) as SNNote; + if (matchingNote) { + this.isTemplateNote = false; + this.note = matchingNote; + this._onNoteValueChange && this._onNoteValueChange!(matchingNote, source); + } + } + + async insertTemplatedNote() { + return this.application.insertItem(this.note); + } + + /** + * Reverts the editor to a blank state, removing any existing note from view, + * and creating a placeholder note. + */ + async reset(noteTitle?: string) { + const note = await this.application.createTemplateItem(ContentType.Note, { + text: '', + title: noteTitle || '', + references: [], + }); + this.isTemplateNote = true; + this.setNote(note as SNNote); + } + + /** + * Register to be notified when the editor's note changes. + */ + public onNoteChange(callback: () => void) { + this._onNoteChange = callback; + if (this.note) { + callback(); + } + } + + /** + * Register to be notified when the editor's note's values change + * (and thus a new object reference is created) + */ + public onNoteValueChange( + callback: (note: SNNote, source?: PayloadSource) => void + ) { + this._onNoteValueChange = callback; + } + + /** + * Sets the editor contents by setting its note. + */ + public setNote(note: SNNote) { + this.note = note; + if (this._onNoteChange) { + this._onNoteChange(); + } + } +} diff --git a/src/lib/EditorGroup.ts b/src/lib/EditorGroup.ts new file mode 100644 index 00000000..bb1be9d5 --- /dev/null +++ b/src/lib/EditorGroup.ts @@ -0,0 +1,74 @@ +import { removeFromArray } from 'snjs'; +import { Editor } from './editor'; +import { MobileApplication } from './application'; + +type EditorGroupChangeCallback = () => void; + +export class EditorGroup { + public editors: Editor[] = []; + private application: MobileApplication; + changeObservers: EditorGroupChangeCallback[] = []; + + constructor(application: MobileApplication) { + this.application = application; + } + + public deinit() { + (this.application as any) = undefined; + for (const editor of this.editors) { + this.deleteEditor(editor); + } + } + + createEditor(noteUuid?: string, noteTitle?: string) { + const editor = new Editor(this.application, noteUuid, noteTitle); + this.editors.push(editor); + this.notifyObservers(); + } + + deleteEditor(editor: Editor) { + editor.deinit(); + removeFromArray(this.editors, editor); + } + + closeEditor(editor: Editor) { + this.deleteEditor(editor); + this.notifyObservers(); + } + + closeActiveEditor() { + const activeEditor = this.activeEditor; + if (activeEditor) { + this.deleteEditor(activeEditor); + } + } + + closeAllEditors() { + for (const editor of this.editors) { + this.deleteEditor(editor); + } + } + + get activeEditor() { + return this.editors[0]; + } + + /** + * Notifies observer when the active editor has changed. + */ + public addChangeObserver(callback: EditorGroupChangeCallback) { + this.changeObservers.push(callback); + if (this.activeEditor) { + callback(); + } + return () => { + removeFromArray(this.changeObservers, callback); + }; + } + + private notifyObservers() { + for (const observer of this.changeObservers) { + observer(); + } + } +} diff --git a/src/lib/OptionsState.ts b/src/lib/OptionsState.ts deleted file mode 100644 index 08c6fe7a..00000000 --- a/src/lib/OptionsState.ts +++ /dev/null @@ -1,174 +0,0 @@ -import _ from 'lodash'; -import Storage from '@Lib/snjs/storageManager'; - -type OptionsStateStateType = - | typeof OptionsState.OptionsStateChangeEventSearch - | typeof OptionsState.OptionsStateChangeEventTags - | typeof OptionsState.OptionsStateChangeEventViews - | typeof OptionsState.OptionsStateChangeEventSort; - -export type Observer = { - key: () => number; - callback: (state: OptionsState, newState?: OptionsStateStateType) => void; -}; - -export default class OptionsState { - static OptionsStateChangeEventSearch = 'OptionsStateChangeEventSearch' as 'OptionsStateChangeEventSearch'; - static OptionsStateChangeEventTags = 'OptionsStateChangeEventTags' as 'OptionsStateChangeEventTags'; - static OptionsStateChangeEventViews = 'OptionsStateChangeEventViews' as 'OptionsStateChangeEventViews'; - static OptionsStateChangeEventSort = 'OptionsStateChangeEventSort' as 'OptionsStateChangeEventSort'; - changeObservers: Observer[]; - sortBy: string; - selectedTagIds: string[]; - sortReverse: boolean; - searchTerm: string | null = null; - displayOptions?: { - hidePreviews: boolean; - hideTags: boolean; - hideDates: boolean; - }; - hidePreviews: boolean = false; - hideDates: boolean = false; - hideTags: boolean = false; - - constructor() { - this.searchTerm = ''; - this.selectedTagIds = []; - this.sortBy = 'created_at'; - this.sortReverse = false; - - // TODO: not used - // _.merge(this, _.omit(json, ['changeObservers'])); - this.changeObservers = []; - - if (this.sortBy === 'updated_at') { - // migrate to client modified date if using old property - this.sortBy = 'client_updated_at'; - } - } - - init() { - this.searchTerm = ''; - this.selectedTagIds = []; - this.sortBy = 'created_at'; - this.sortReverse = false; - } - - reset(notifyObservers = true) { - this.init(); - if (notifyObservers) { - this.notifyObservers(); - } - } - - async loadSaved() { - return Storage.get() - .getItem('options') - .then(result => { - if (result) { - _.merge(this, _.omit(JSON.parse(result), ['changeObservers'])); - } - this.rebuildOptions(); - this.notifyObservers(); - }); - } - - persist() { - Storage.get().setItem('options', JSON.stringify(this)); - } - - toJSON() { - return _.merge( - { - sortBy: this.sortBy, - sortReverse: this.sortReverse, - selectedTagIds: this.selectedTagIds, - }, - this.getDisplayOptionValues() - ); - } - - addChangeObserver(callback: Observer['callback']) { - const observer = { key: Math.random, callback }; - this.changeObservers.push(observer); - return observer; - } - - removeChangeObserver(observer: Observer) { - _.pull(this.changeObservers, observer); - } - - notifyObservers(newOption?: OptionsStateStateType) { - this.changeObservers.forEach( - function (observer: Observer) { - // @ts-ignore - observer.callback(this as OptionsState, newOption); - }.bind(this) - ); - } - - // Interface - setSearchTerm(term: string | null) { - this.searchTerm = term; - this.notifyObservers(OptionsState.OptionsStateChangeEventSearch); - } - - setSortReverse(reverse: boolean) { - this.sortReverse = reverse; - this.notifyObservers(OptionsState.OptionsStateChangeEventSort); - } - - setSortBy(sortBy: string) { - this.sortBy = sortBy; - this.notifyObservers(OptionsState.OptionsStateChangeEventSort); - } - - setSelectedTagIds(selectedTagIds: string[]) { - this.selectedTagIds = selectedTagIds; - this.notifyObservers(OptionsState.OptionsStateChangeEventTags); - } - - getSelectedTagIds() { - return this.selectedTagIds; - } - - getDisplayOptionValues() { - if (!this.displayOptions) { - this.rebuildOptions(); - } - return this.displayOptions; - } - - rebuildOptions() { - this.displayOptions = { - hidePreviews: this.getDisplayOptionValue('hidePreviews'), - hideTags: this.getDisplayOptionValue('hideTags'), - hideDates: this.getDisplayOptionValue('hideDates'), - }; - } - - getDisplayOptionValue(key: string) { - if (key === 'hidePreviews') { - return this.hidePreviews; - } else if (key === 'hideDates') { - return this.hideDates; - } else if (key === 'hideTags') { - return this.hideTags; - } - return false; - } - - setDisplayOptionKeyValue(key: string, value: any) { - if (key === 'hidePreviews') { - this.hidePreviews = value; - } else if (key === 'hideDates') { - this.hideDates = value; - } else if (key === 'hideTags') { - this.hideTags = value; - } - - this.rebuildOptions(); - - this.notifyObservers(OptionsState.OptionsStateChangeEventViews); - } -} diff --git a/src/lib/PreferencesManager.ts b/src/lib/PreferencesManager.ts new file mode 100644 index 00000000..27a6f263 --- /dev/null +++ b/src/lib/PreferencesManager.ts @@ -0,0 +1,80 @@ +import { MobileApplication } from './application'; +import { + SNPredicate, + ContentType, + ApplicationService, + SNUserPrefs, + WebPrefKey, + UserPrefsMutator, + FillItemContent, +} from 'snjs'; + +export class PreferencesManager extends ApplicationService { + private userPreferences!: SNUserPrefs; + private loadingPrefs = false; + + /** @override */ + async onAppLaunch() { + super.onAppLaunch(); + this.streamPreferences(); + } + + get mobileApplication() { + return this.application as MobileApplication; + } + + streamPreferences() { + this.application!.streamItems(ContentType.UserPrefs, () => { + this.loadSingleton(); + }); + } + + private async loadSingleton() { + if (this.loadingPrefs) { + return; + } + this.loadingPrefs = true; + const contentType = ContentType.UserPrefs; + const predicate = new SNPredicate('content_type', '=', contentType); + const previousRef = this.userPreferences; + this.userPreferences = (await this.application!.singletonManager!.findOrCreateSingleton( + predicate, + contentType, + FillItemContent({}) + )) as SNUserPrefs; + this.loadingPrefs = false; + const didChange = + !previousRef || + this.userPreferences.lastSyncBegan?.getTime() !== + previousRef?.lastSyncBegan?.getTime(); + if (didChange) { + this.mobileApplication + .getAppState() + .setUserPreferences(this.userPreferences); + } + } + + syncUserPreferences() { + if (this.userPreferences) { + this.application!.saveItem(this.userPreferences.uuid); + } + } + + getValue(key: WebPrefKey, defaultValue?: any) { + if (!this.userPreferences) { + return defaultValue; + } + const value = this.userPreferences.getPref(key); + return value !== undefined && value !== null ? value : defaultValue; + } + + async setUserPrefValue(key: WebPrefKey, value: any, sync = false) { + await this.application!.changeItem(this.userPreferences.uuid, m => { + const mutator = m as UserPrefsMutator; + mutator.setWebPref(key, value); + }); + if (sync) { + this.syncUserPreferences(); + } + } +} diff --git a/src/lib/application.ts b/src/lib/application.ts new file mode 100644 index 00000000..de9e90a8 --- /dev/null +++ b/src/lib/application.ts @@ -0,0 +1,91 @@ +import { + Environment, + SNApplication, + SNAlertService, + platformFromString, +} from 'snjs'; +import { MobileDeviceInterface } from './interface'; +import { AlertService } from './AlertService'; +import { ApplicationState } from './ApplicationState'; +import { Platform } from 'react-native'; +import { EditorGroup } from './EditorGroup'; +import { ComponentGroup } from './componentGroup'; +import { ReviewService } from './reviewService'; +import { BackupsService } from './BackupsService'; +import { PreferencesManager } from './PreferencesManager'; + +type MobileServices = { + applicationState: ApplicationState; + reviewService: ReviewService; + backupsService: BackupsService; + themeService: ThemeManager; + prefsService: PreferencesManager; +}; + +export class MobileApplication extends SNApplication { + private onDeinit?: (app: MobileApplication) => void; + private MobileServices!: MobileServices; + public editorGroup: EditorGroup; + public componentGroup: ComponentGroup; + + constructor(onDeinit: (app: MobileApplication) => void) { + const namespace = ''; + const deviceInterface = new MobileDeviceInterface(namespace); + super( + Environment.Mobile, + platformFromString(Platform.OS), + deviceInterface, + namespace, + undefined, + [ + { + swap: SNAlertService, + with: AlertService, + }, + ] + ); + this.onDeinit = onDeinit; + this.editorGroup = new EditorGroup(this); + this.componentGroup = new ComponentGroup(this); + } + + deinit() { + for (const key of Object.keys(this.MobileServices)) { + const service = (this.MobileServices as any)[key]; + if (service.deinit) { + service.deinit(); + } + service.application = undefined; + } + this.MobileServices = {} as MobileServices; + this.onDeinit!(this); + this.onDeinit = undefined; + this.editorGroup.deinit(); + this.componentGroup.deinit(); + super.deinit(); + } + + setMobileServices(services: MobileServices) { + this.MobileServices = services; + } + + public getAppState() { + return this.MobileServices.applicationState; + } + + public getThemeService() { + return this.MobileServices.themeService; + } + + public getBackupsService() { + return this.MobileServices.backupsService; + } + + public getPrefsService() { + return this.MobileServices.prefsService; + } + + async checkForSecurityUpdate() { + return this.protocolUpgradeAvailable(); + } +} diff --git a/src/lib/applicationGroup.ts b/src/lib/applicationGroup.ts new file mode 100644 index 00000000..8d868ddf --- /dev/null +++ b/src/lib/applicationGroup.ts @@ -0,0 +1,85 @@ +import { MobileApplication } from './application'; +import { removeFromArray } from 'snjs'; +import { ApplicationState } from './ApplicationState'; + +type AppManagerChangeCallback = () => void; + +export class ApplicationGroup { + applications: MobileApplication[] = []; + changeObservers: AppManagerChangeCallback[] = []; + activeApplication?: MobileApplication; + + constructor() { + this.onApplicationDeinit = this.onApplicationDeinit.bind(this); + this.createDefaultApplication(); + } + + private createDefaultApplication() { + this.activeApplication = this.createNewApplication(); + this.applications.push(this.activeApplication!); + this.notifyObserversOfAppChange(); + } + + /** @callback */ + onApplicationDeinit(application: MobileApplication) { + removeFromArray(this.applications, application); + if (this.activeApplication === application) { + this.activeApplication = undefined; + } + if (this.applications.length === 0) { + this.createDefaultApplication(); + } + this.notifyObserversOfAppChange(); + } + + private createNewApplication() { + const application = new MobileApplication(this.onApplicationDeinit); + const applicationState = new ApplicationState(application); + const archiveService = new ArchiveManager(application); + const desktopService = new DesktopManager(application); + const keyboardService = new KeyboardManager(); + const lockService = new LockManager(application); + const nativeExtService = new NativeExtManager(application); + const prefsService = new PreferencesManager(application); + const statusService = new StatusManager(); + const themeService = new ThemeManager(application); + application.setMobileServices({ + applicationState, + archiveService, + desktopService, + keyboardService, + lockService, + nativeExtService, + prefsService, + statusService, + themeService, + }); + 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(); + } + } + + private notifyObserversOfAppChange() { + for (const observer of this.changeObservers) { + observer(); + } + } +} diff --git a/src/lib/componentGroup.ts b/src/lib/componentGroup.ts new file mode 100644 index 00000000..5d87f293 --- /dev/null +++ b/src/lib/componentGroup.ts @@ -0,0 +1,100 @@ +import { SNComponent, ComponentArea, removeFromArray, addIfUnique } from 'snjs'; +import { MobileApplication } from './application'; +import { UuidString } from 'snjs/dist/@types/types'; + +/** Areas that only allow a single component to be active */ +const SingleComponentAreas = [ + ComponentArea.Editor, + ComponentArea.NoteTags, + ComponentArea.TagsList, +]; + +export class ComponentGroup { + private application: MobileApplication; + changeObservers: any[] = []; + activeComponents: UuidString[] = []; + + constructor(application: MobileApplication) { + this.application = application; + } + + get componentManager() { + return this.application?.componentManager!; + } + + public deinit() { + (this.application as any) = undefined; + } + + async activateComponent(component: SNComponent) { + if (this.activeComponents.includes(component.uuid)) { + return; + } + if (SingleComponentAreas.includes(component.area)) { + const currentActive = this.activeComponentForArea(component.area); + if (currentActive) { + await this.deactivateComponent(currentActive, false); + } + } + addIfUnique(this.activeComponents, component.uuid); + await this.componentManager.activateComponent(component.uuid); + this.notifyObservers(); + } + + async deactivateComponent(component: SNComponent, notify = true) { + if (!this.activeComponents.includes(component.uuid)) { + return; + } + removeFromArray(this.activeComponents, component.uuid); + /** If this function is called as part of global application deinit (locking), + * componentManager can be destroyed. In this case, it's harmless to not take any + * action since the componentManager will be destroyed, and the component will + * essentially be deregistered. */ + if (this.componentManager) { + await this.componentManager.deactivateComponent(component.uuid); + if (notify) { + this.notifyObservers(); + } + } + } + + async deactivateComponentForArea(area: ComponentArea) { + const component = this.activeComponentForArea(area); + if (component) { + return this.deactivateComponent(component); + } + } + + activeComponentForArea(area: ComponentArea) { + return this.activeComponentsForArea(area)[0]; + } + + activeComponentsForArea(area: ComponentArea) { + return this.allActiveComponents().filter(c => c.area === area); + } + + allComponentsForArea(area: ComponentArea) { + return this.componentManager.componentsForArea(area); + } + + private allActiveComponents() { + return this.application.getAll(this.activeComponents) as SNComponent[]; + } + + /** + * Notifies observer when the active editor has changed. + */ + public addChangeObserver(callback: () => void) { + this.changeObservers.push(callback); + callback(); + return () => { + removeFromArray(this.changeObservers, callback); + }; + } + + private notifyObservers() { + for (const observer of this.changeObservers) { + observer(); + } + } +} diff --git a/src/lib/componentManager.ts b/src/lib/componentManager.ts deleted file mode 100644 index 6aa4e111..00000000 --- a/src/lib/componentManager.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { Platform } from 'react-native'; -import { SNComponentManager } from 'snjs'; -import AlertManager from '@Lib/snjs/alertManager'; -import ModelManager from '@Lib/snjs/modelManager'; -import Sync from '@Lib/snjs/syncManager'; -import StyleKit from '@Style/StyleKit'; - -type ComponentManagerInstance = { - modelManager: ModelManager; - syncManager: Sync; - alertManager: AlertManager; - environment: 'mobile'; - platform: typeof Platform.OS; - desktopManager?: any; - nativeExtManager?: any; - $uiRunner?: any; -}; - -export default class ComponentManager extends SNComponentManager { - private static instance: ComponentManager; - - static get() { - if (!this.instance) { - this.instance = new ComponentManager({ - modelManager: ModelManager.get(), - syncManager: Sync.get(), - alertManager: AlertManager.get(), - environment: 'mobile', - platform: Platform.OS, - }); - } - - return this.instance; - } - - constructor({ - modelManager, - syncManager, - desktopManager, - nativeExtManager, - alertManager, - $uiRunner, - platform, - environment, - }: ComponentManagerInstance) { - super({ - modelManager, - syncManager, - desktopManager, - nativeExtManager, - alertManager, - $uiRunner, - platform, - environment, - }); - } - - /* - Overrides - */ - - urlsForActiveThemes() { - const theme = StyleKit.get().activeTheme; - if (theme.content.isSystemTheme) { - return null; - } - - if (theme) { - const url = this.urlForComponent(theme); - return [url]; - } - } - - /* - @param {object} dialog: {permissions, String, component, callback} - */ - - presentPermissionsDialog(dialog: { - component: { name: any }; - permissionsString: any; - callback: (arg0: boolean) => void; - }) { - let text = `${dialog.component.name} would like to interact with your ${dialog.permissionsString}`; - this.alertManager.confirm({ - title: 'Grant Permissions', - text: text, - confirmButtonText: 'Continue', - cancelButtonText: 'Cancel', - onConfirm: () => { - dialog.callback(true); - }, - onCancel: () => { - dialog.callback(false); - }, - }); - } - - /* - Custom functions, not overrides - */ - - getEditors() { - return this.componentsForArea('editor-editor'); - } - - getDefaultEditor() { - return this.getEditors().filter( - (e: { content: { isMobileDefault: any } }) => { - return e.content.isMobileDefault; - } - )[0]; - } - - setEditorAsMobileDefault( - editor: { - content: { isMobileDefault: any }; - setDirty?: (arg0: boolean) => void; - }, - isDefault: boolean - ) { - if (isDefault) { - // Remove current default - const currentDefault = this.getDefaultEditor(); - if (currentDefault) { - currentDefault.content.isMobileDefault = false; - currentDefault.setDirty(true); - } - } - - // Could be null if plain editor - if (editor) { - editor.content.isMobileDefault = isDefault; - editor.setDirty && editor.setDirty(true); - } - Sync.get().sync(); - } - - associateEditorWithNote( - editor: { - disassociatedItemIds: any[]; - associatedItemIds: any[]; - setDirty: (arg0: boolean) => void; - } | null, - note: { - uuid: any; - content: { mobilePrefersPlainEditor: boolean }; - setDirty: (arg0: boolean) => void; - } - ) { - const currentEditor = this.editorForNote(note); - if (currentEditor && currentEditor !== editor) { - // Disassociate currentEditor with note - currentEditor.associatedItemIds = currentEditor.associatedItemIds.filter( - (id: any) => { - return id !== note.uuid; - } - ); - - if (!currentEditor.disassociatedItemIds.includes(note.uuid)) { - currentEditor.disassociatedItemIds.push(note.uuid); - } - - currentEditor.setDirty(true); - } - - if (editor) { - if (note.content.mobilePrefersPlainEditor === true) { - note.content.mobilePrefersPlainEditor = false; - note.setDirty(true); - } - - editor.disassociatedItemIds = editor.disassociatedItemIds.filter(id => { - return id !== note.uuid; - }); - - if (!editor.associatedItemIds.includes(note.uuid)) { - editor.associatedItemIds.push(note.uuid); - } - - editor.setDirty(true); - } else { - // Note prefers plain editor - if (!note.content.mobilePrefersPlainEditor) { - note.content.mobilePrefersPlainEditor = true; - note.setDirty(true); - } - } - - Sync.get().sync(); - } - - clearEditorForNote(note: { - uuid: any; - content: { mobilePrefersPlainEditor: boolean }; - setDirty: (arg0: boolean) => void; - }) { - this.associateEditorWithNote(null, note); - } -} diff --git a/src/lib/interface.ts b/src/lib/interface.ts new file mode 100644 index 00000000..cddb363e --- /dev/null +++ b/src/lib/interface.ts @@ -0,0 +1,126 @@ +import { DeviceInterface } from 'snjs'; +import AsyncStorage from '@react-native-community/async-storage'; +import { Platform } from 'react-native'; +import ApplicationState from './ApplicationState'; +import Keychain from './keychain'; + +export class MobileDeviceInterface extends DeviceInterface { + constructor(namespace: string) { + super(namespace, setTimeout, setInterval); + } + + deinit() { + super.deinit(); + } + + private getDatabaseKeyPrefix() { + if (this.namespace) { + return `${this.namespace}-Item-`; + } else { + return 'Item-'; + } + } + + private keyForPayloadId(id: string) { + return `${this.getDatabaseKeyPrefix()}${id}`; + } + + private async getAllDatabaseKeys() { + const keys = await AsyncStorage.getAllKeys(); + const filtered = keys.filter(key => { + return key.includes(this.getDatabaseKeyPrefix()); + }); + return filtered; + } + + private async getRawStorageKeyValues(keys: string[]) { + if (Platform.OS === 'android') { + const results = []; + for (const key of keys) { + try { + const item = await AsyncStorage.getItem(key); + if (item) { + results.push({ + key, + value: item, + }); + } + } catch (e) { + console.log('Error getting item', key, e); + } + } + } else { + try { + return AsyncStorage.multiGet(keys); + } catch (e) { + console.log('Error getting items', keys, e); + } + } + + return []; + } + + getRawStorageValue(key: string) { + return AsyncStorage.getItem(key); + } + + async getAllRawStorageKeyValues(): Promise[]> { + const keys = await AsyncStorage.getAllKeys(); + return this.getRawStorageKeyValues(keys); + } + setRawStorageValue(key: string, value: any): Promise { + return AsyncStorage.setItem(key, value); + } + removeRawStorageValue(key: string): Promise { + return AsyncStorage.removeItem(key); + } + removeAllRawStorageValues(): Promise { + return AsyncStorage.clear(); + } + openDatabase(): Promise<{ isNewDatabase?: boolean | undefined } | undefined> { + // TODO: check if items do not have to be redownloaded in case of a failure + return Promise.resolve({ isNewDatabase: false }); + } + + async getAllRawDatabasePayloads(): Promise { + const keys = await this.getAllDatabaseKeys(); + return this.getRawStorageKeyValues(keys); + } + saveRawDatabasePayload(payload: any): Promise { + return this.saveRawDatabasePayloads([payload]); + } + async saveRawDatabasePayloads(payloads: any[]): Promise { + if (payloads.length === 0) { + return; + } + + await Promise.all( + payloads.map(item => { + return AsyncStorage.setItem( + this.keyForPayloadId(item), + JSON.stringify(item) + ); + }) + ); + } + removeRawDatabasePayloadWithId(id: string): Promise { + return this.removeRawStorageValue(this.keyForPayloadId(id)); + } + async removeAllRawDatabasePayloads(): Promise { + const keys = await this.getAllDatabaseKeys(); + return AsyncStorage.multiRemove(keys.map(key => this.keyForPayloadId(key))); + } + getKeychainValue(): Promise { + return Keychain.getKeys(); + } + setKeychainValue(value: any): Promise { + return Keychain.setKeys(value); + } + clearKeychainValue(): Promise { + return Keychain.clearKeys(); + } + + openUrl(url: string) { + return ApplicationState.openURL(url); + } +} diff --git a/src/lib/itemActionManager.ts b/src/lib/itemActionManager.ts deleted file mode 100644 index aacf2e31..00000000 --- a/src/lib/itemActionManager.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { Share } from 'react-native'; -import ApplicationState from '@Lib/ApplicationState'; -import AlertManager from '@Lib/snjs/alertManager'; -import ModelManager from '@Lib/snjs/modelManager'; -import Sync from '@Lib/snjs/syncManager'; - -export type EventType = - | typeof ItemActionManager.DeleteEvent - | typeof ItemActionManager.TrashEvent - | typeof ItemActionManager.RestoreEvent - | typeof ItemActionManager.EmptyTrashEvent - | typeof ItemActionManager.PinEvent - | typeof ItemActionManager.UnpinEvent - | typeof ItemActionManager.ArchiveEvent - | typeof ItemActionManager.UnarchiveEvent - | typeof ItemActionManager.LockEvent - | typeof ItemActionManager.UnlockEvent - | typeof ItemActionManager.ProtectEvent - | typeof ItemActionManager.UnprotectEvent - | typeof ItemActionManager.ShareEvent; - -export default class ItemActionManager { - static DeleteEvent = 'DeleteEvent' as 'DeleteEvent'; - static TrashEvent = 'TrashEvent' as 'TrashEvent'; - static RestoreEvent = 'RestoreEvent' as 'RestoreEvent'; - static EmptyTrashEvent = 'EmptyTrashEvent' as 'EmptyTrashEvent'; - - static PinEvent = 'PinEvent' as 'PinEvent'; - static UnpinEvent = 'UnpinEvent' as 'UnpinEvent'; - - static ArchiveEvent = 'ArchiveEvent' as 'ArchiveEvent'; - static UnarchiveEvent = 'UnarchiveEvent' as 'UnarchiveEvent'; - - static LockEvent = 'LockEvent' as 'LockEvent'; - static UnlockEvent = 'UnlockEvent' as 'UnlockEvent'; - - static ProtectEvent = 'ProtectEvent' as 'ProtectEvent'; - static UnprotectEvent = 'UnprotectEvent' as 'UnprotectEvent'; - - static ShareEvent = 'ShareEvent' as 'ShareEvent'; - - /* The afterConfirmCallback is called after user confirms deletion pop up */ - - static handleEvent( - event: EventType, - item: { - displayName: string; - content: { trashed: boolean; protected: boolean }; - setDirty: (arg0: boolean) => void; - setAppDataItem: (arg0: string, arg1: boolean) => void; - title: any; - text: any; - }, - callback: { (): any }, - afterConfirmCallback?: () => void - ) { - if (event === this.TrashEvent) { - const title = 'Move to Trash'; - const message = `Are you sure you want to move this ${item.displayName.toLowerCase()} to the trash?`; - - AlertManager.get().confirm({ - title: title, - text: message, - confirmButtonText: 'Confirm', - onConfirm: () => { - item.content.trashed = true; - item.setDirty(true); - Sync.get().sync(); - callback && callback(); - }, - }); - } else if (event === this.EmptyTrashEvent) { - let deletedCount = ModelManager.get().trashedItems().length; - AlertManager.get().confirm({ - title: 'Empty Trash', - text: `Are you sure you want to permanently delete ${deletedCount} notes?`, - confirmButtonText: 'Delete', - onConfirm: () => { - ModelManager.get().emptyTrash(); - Sync.get().sync(); - callback && callback(); - }, - }); - } else if (event === this.DeleteEvent) { - var title = `Delete ${item.displayName}`; - var message = `Are you sure you want to permanently delete this ${item.displayName.toLowerCase()}?`; - - AlertManager.get().confirm({ - title: title, - text: message, - confirmButtonText: 'Delete', - onConfirm: () => { - ModelManager.get().setItemToBeDeleted(item); - - afterConfirmCallback && afterConfirmCallback(); - - Sync.get() - .sync() - .then(() => { - callback && callback(); - }); - }, - }); - } else if (event === this.RestoreEvent) { - item.content.trashed = false; - item.setDirty(true); - Sync.get().sync(); - callback && callback(); - } else if (event === this.PinEvent || event === this.UnpinEvent) { - item.setAppDataItem('pinned', event === this.PinEvent); - item.setDirty(true); - Sync.get().sync(); - callback && callback(); - } else if (event === this.LockEvent || event === this.UnlockEvent) { - item.setAppDataItem('locked', event === this.LockEvent); - item.setDirty(true); - Sync.get().sync(); - callback && callback(); - } else if (event === this.ArchiveEvent || event === this.UnarchiveEvent) { - item.setAppDataItem('archived', event === this.ArchiveEvent); - item.setDirty(true); - Sync.get().sync(); - callback && callback(); - } else if (event === this.ProtectEvent || event === this.UnprotectEvent) { - item.content.protected = !item.content.protected; - item.setDirty(true); - Sync.get().sync(); - callback && callback(); - } else if (event === this.ShareEvent) { - ApplicationState.get().performActionWithoutStateChangeImpact(() => { - Share.share({ - title: item.title, - message: item.text, - }); - }); - callback && callback(); - } - } -} diff --git a/src/lib/keysManager.ts b/src/lib/keysManager.ts deleted file mode 100644 index b5b3df56..00000000 --- a/src/lib/keysManager.ts +++ /dev/null @@ -1,658 +0,0 @@ -import { Platform, Alert } from 'react-native'; -import FlagSecure from 'react-native-flag-secure-android'; -import FingerprintScanner from 'react-native-fingerprint-scanner'; -import SNReactNative from 'standard-notes-rn'; -import { SFItemParams, SFItem, protocolManager } from 'snjs'; -import _ from 'lodash'; - -import Keychain from '@Lib/keychain'; -import { isNullOrUndefined } from '@Lib/utils'; -import AlertManager from '@Lib/snjs/alertManager'; -import Storage from '@Lib/snjs/storageManager'; - -const OfflineParamsKey = 'pc_params'; -const EncryptedAccountKeysKey = 'encrypted_account_keys'; -const BiometricsPrefs = 'biometrics_prefs'; -const FirstRunKey = 'first_run'; -const StorageEncryptionKey = 'storage_encryption'; - -type AccountKeys = { - fingerprint: { - enabled: boolean; - timing: any; - }; - encryptedAccountKeys: any; - mk?: string; - ak?: string; -} | null; - -type User = { server?: string; email?: string; jwt?: string | null } | null; - -export type BiometricsType = - | 'Fingerprint' - | 'Face ID' - | 'Biometrics' - | 'Touch ID'; - -export default class KeysManager { - private static instance: KeysManager; - passcodeTiming: string | null = null; - biometricPrefs: any; - accountRelatedStorageKeys: string[]; - legacy_fingerprint: any; - encryptedAccountKeys: any; - accountKeys: AccountKeys = null; - loadInitialDataPromise: Promise | undefined; - user: User = null; - missingFirstRunKey: boolean = false; - accountAuthParams: any; - offlineAuthParams: any; - _storageEncryptionEnabled: boolean = false; - offlineKeys: any; - - static get() { - if (!this.instance) { - this.instance = new KeysManager(); - } - - return this.instance; - } - - constructor() { - this.biometricPrefs = {}; - this.accountRelatedStorageKeys = ['auth_params', 'user']; - } - - async runPendingMigrations() { - const biometricsMigrationName = '10102019KeychainToStorage'; - if ((await Storage.get().getItem(biometricsMigrationName)) == null) { - console.log('Running migration', biometricsMigrationName); - await this.migrateBiometricsPrefsToStorage(); - await Storage.get().setItem(biometricsMigrationName, 'true'); - } - - const jwtUndoMigration = '10232019JwtToKeychain'; - if ((await Storage.get().getItem(jwtUndoMigration)) == null) { - console.log('Running migration', jwtUndoMigration); - await this.undoJwtMigration(); - await Storage.get().setItem(jwtUndoMigration, 'true'); - } - } - - async migrateBiometricsPrefsToStorage() { - // Move biometrics preference to storage - if (this.legacy_fingerprint) { - this.biometricPrefs.enabled = this.legacy_fingerprint.enabled; - this.biometricPrefs.timing = this.legacy_fingerprint.timing; - await this.saveBiometricPrefs(); - this.legacy_fingerprint = null; - } - - if (this.legacy_fingerprint) { - await this.persistKeys(); - } - } - - /* - In 10102019KeychainToStorage, we migrated JWT from being stored the keychain to being stored in storage. - This was so that when the user uninstalled the app, their JWT would be wiped (rather than retained in the keychain). - However, it would be more secure to keep JWT in keychain, even if it persists between uninstalls. - */ - async undoJwtMigration() { - let jwt = this.user && this.user.jwt; - if (!jwt) { - return; - } - - this.user = this.user ? this.user : {}; - this.user.jwt = null; - this.activeKeys().jwt = jwt; - await this.saveUser(this.user); - await this.persistKeys(); - } - - async loadInitialData() { - const storageKeys = [ - 'auth_params', - 'user', - OfflineParamsKey, - EncryptedAccountKeysKey, - FirstRunKey, - StorageEncryptionKey, - BiometricsPrefs, - ]; - - /* - We only want to call this once per app session. On Android, the App.tsx component may be unmounted - on hardware back button press. When it constructs again, it calls this function, resetting our values for offlineKeys - which we don't want to change, since on authentication, they are set by the passcode unlock success function. - */ - if (this.loadInitialDataPromise) { - return this.loadInitialDataPromise; - } - - this.loadInitialDataPromise = Promise.all([ - Keychain.getKeys().then(keys => { - if (keys) { - this.parseKeychainValue(keys); - } - }), - - Storage.get() - .getMultiItems(storageKeys) - .then(async items => { - this.missingFirstRunKey = - items[FirstRunKey] === null || items[FirstRunKey] === undefined; - - // auth params - var authParams = items.auth_params; - if (authParams) { - this.accountAuthParams = JSON.parse(authParams); - } - - let biometricPrefs = items[BiometricsPrefs]; - if (biometricPrefs) { - this.biometricPrefs = JSON.parse(biometricPrefs); - } - - // offline params - var pcParams = items[OfflineParamsKey]; - if (pcParams) { - this.offlineAuthParams = JSON.parse(pcParams); - } - - let encryptedAccountKeys = items[EncryptedAccountKeysKey]; - if (encryptedAccountKeys) { - this.encryptedAccountKeys = JSON.parse(encryptedAccountKeys); - } - - // storage encryption - if (isNullOrUndefined(items[StorageEncryptionKey])) { - // default is true - // Note that this defaults to true, but doesn't dictate if it's actually applied. - // For example, when you first install the app and have no account and no passcode, - // There will be no encryption source available. In the syncManager key request handler, - // we check if this flag is true and if there are active keys. We use this to indicate that - // when keys become available, we want storage encryption to be enabled by default. - this._storageEncryptionEnabled = true; - } else { - this._storageEncryptionEnabled = - JSON.parse(items[StorageEncryptionKey]) === true; - } - - // user - var user = items.user; - if (user) { - this.user = JSON.parse(user); - } else { - this.user = {}; - } - }), - ]).then(async () => { - // We only want to run migrations in unlocked app state. If account keys are present, run now, - // otherwise wait until offline keys have been set so that account keys are decrypted. - if (!this.encryptedAccountKeys) { - return this.runPendingMigrations(); - } - }); - - return this.loadInitialDataPromise; - } - - async needsWipe() { - // Needs wipe if has keys but no data. However, since "no data" can be incorrectly reported by underlying - // AsyncStorage failures, we want to confirm with the user before deleting anything. - - let hasKeys = this.activeKeys() != null; - let noData = this.missingFirstRunKey === true; - return hasKeys && noData; - } - - async markApplicationAsRan() { - return Storage.get().setItem(FirstRunKey, 'false'); - } - - async wipeData() { - // On iOS, keychain data is persisted between installs/uninstalls. (https://stackoverflow.com/questions/4747404/delete-keychain-items-when-an-app-is-uninstalled) - // This prevents the user from deleting the app and reinstalling if they forgot their local passocde - // or if fingerprint scanning isn't working. By deleting all data on first run, we allow the user to reset app - // state after uninstall. - - console.log('===Wiping Data==='); - - return AlertManager.get().confirm({ - title: 'Previous Installation', - text: - "We've detected a previous installation of Standard Notes based on your keychain data. You must wipe all data from previous installation to continue.\n\nIf you're seeing this message in error, it might mean we're having issues loading your local database. Please restart the app and try again.", - confirmButtonText: 'Delete Local Data', - cancelButtonText: 'Quit App', - onConfirm: async () => { - await Storage.get().clear(); - await Keychain.clearKeys(); - this.parseKeychainValue(null); - this.accountAuthParams = null; - this.user = null; - }, - onCancel: () => { - SNReactNative.exitApp(); - }, - }); - } - - /* - If a user was using the app offline without an account, and had a local passcode, then did - an iCloud restore, then they will have saved auth params (saved to storage), - but no keychain values (not saved to storage). - - In this case, we want to present a recovery wizard where they can attempt values of their local passcode, - and see if it yeilds successful decryption. We can't verify whether the passcode they enter is correct, - since the valid hash value is stored in the keychain as well. - */ - shouldPresentKeyRecoveryWizard() { - if ( - !this.accountAuthParams && - this.offlineAuthParams && - !this.offlineKeys - ) { - return true; - } else { - return false; - } - } - - /* - We need to register local storage keys, so that when we want to sign out, we don't accidentally - clear internal keys, like first_run. (If you accidentally delete the first_run key when you sign out, - then the next time you sign in and refresh, it will treat it as a new run, and delete all data.) - */ - registerAccountRelatedStorageKeys(storageKeys: ConcatArray) { - this.accountRelatedStorageKeys = _.uniq( - this.accountRelatedStorageKeys.concat(storageKeys) - ); - } - - parseKeychainValue( - keys: { - offline: boolean; - fingerprint: { enabled: boolean; timing: any }; - encryptedAccountKeys: any; - } | null - ) { - if (keys) { - this.offlineKeys = keys.offline; - if (this.offlineKeys) { - this.passcodeTiming = this.offlineKeys.timing; - } - - // Legacy, migrated to Storage - if (keys.fingerprint) { - this.legacy_fingerprint = keys.fingerprint; - this.biometricPrefs.enabled = keys.fingerprint.enabled; - this.biometricPrefs.timing = keys.fingerprint.timing; - delete keys.fingerprint; - } - - if (keys.encryptedAccountKeys) { - // LEGACY: storing encryptedAccountKeys in keychain is legacy behavior. Now stored in storage. - // We won't handle this case here. We'll wait until setOfflineKeys is called - // by whoever authenticates local passcode. That's when we actually get the offline - // keys we can use to decrypt encryptedAccountKeys - this.encryptedAccountKeys = keys.encryptedAccountKeys; - } else { - this.accountKeys = _.omit(keys, ['offline']); - if (_.keys(this.accountKeys).length === 0) { - this.accountKeys = null; - } - } - } else { - this.offlineKeys = null; - this.passcodeTiming = null; - this.accountKeys = null; - } - } - - // what we should write to keychain - async generateKeychainStoreValue(): Promise< - {} | AccountKeys | { offline: { pw: any; timing: string | null } } - > { - let value = {}; - - // If no offline keys, store account keys directly. Otherwise we'll encrypt account keys and store in storage. - if (this.accountKeys && !this.offlineKeys) { - _.merge(value, this.accountKeys); - } - - if (this.offlineKeys) { - _.merge(value, { - offline: { pw: this.offlineKeys.pw, timing: this.passcodeTiming }, - }); - } - - return value; - } - - async persistKeys() { - // This funciton is called when changes are made to authentication state - this.updateScreenshotPrivacy(); - - if (this.accountKeys && this.offlineKeys) { - // If offline local passcode keys are available, use that to encrypt account keys - // Don't encrypt offline pw because then we don't be able to verify passcode - const encryptedKeys = new SFItem(); - encryptedKeys.initUUID(); - encryptedKeys.content_type = 'SN|Mobile|EncryptedKeys'; - encryptedKeys.content.accountKeys = this.accountKeys; - const params = new SFItemParams( - encryptedKeys, - this.offlineKeys, - this.offlineAuthParams - ); - const results = await params.paramsForSync(); - await Storage.get().setItem( - EncryptedAccountKeysKey, - JSON.stringify(results) - ); - } else { - await Storage.get().removeItem(EncryptedAccountKeysKey); - } - - let value = await this.generateKeychainStoreValue(); - // TODO: check keychain - // @ts-ignore don't want to change that - return Keychain.setKeys(value); - } - - updateScreenshotPrivacy() { - if (Platform.OS === 'ios') { - return; - } - - const hasImmediatePasscode = - this.hasOfflinePasscode() && this.passcodeTiming === 'immediately'; - const hasImmedateBiometrics = - this.hasBiometrics() && this.biometricPrefs.timing === 'immediately'; - const enabled = hasImmediatePasscode || hasImmedateBiometrics; - - if (enabled) { - FlagSecure.activate(); - } else { - FlagSecure.deactivate(); - } - } - - async persistAccountKeys(keys: AccountKeys) { - this.accountKeys = keys; - return this.persistKeys(); - } - - async saveUser(user: User) { - this.user = user; - return Storage.get().setItem('user', JSON.stringify(user)); - } - - /* The keys to use for encryption. If user is signed in, use those keys, otherwise use offline keys */ - activeKeys() { - if (this.hasAccountKeys()) { - return this.accountKeys; - } else { - return this.offlineKeys; - } - } - - hasAccountKeys() { - return this.accountKeys && this.accountKeys.mk != null; - } - - isOfflineEncryptionEnabled() { - var keys = this.activeKeys(); - return keys && keys.mk !== null && this.isStorageEncryptionEnabled(); - } - - encryptionSource() { - if (this.accountKeys && this.accountKeys.mk !== null) { - return 'account'; - } else if (this.offlineKeys && this.offlineKeys.mk !== null) { - return 'offline'; - } else { - return null; - } - } - - async clearAccountKeysAndData() { - Keychain.clearKeys(); - this.accountKeys = null; - this.accountAuthParams = null; - this.user = null; - await Storage.get().clearKeys(this.accountRelatedStorageKeys); - return this.persistKeys(); - } - - jwt() { - let keys = this.activeKeys(); - return keys && keys.jwt; - } - - // Storage Encryption - - async enableStorageEncryption() { - this._storageEncryptionEnabled = true; - return Storage.get().setItem( - StorageEncryptionKey, - JSON.stringify(this._storageEncryptionEnabled) - ); - } - - async disableStorageEncryption() { - this._storageEncryptionEnabled = false; - return Storage.get().setItem( - StorageEncryptionKey, - JSON.stringify(this._storageEncryptionEnabled) - ); - } - - isStorageEncryptionEnabled() { - // See comment in loadInitialData regarding why the value of this._storageEncryptionEnabled is not sufficient - // to determine whether it's actually enabled - return this._storageEncryptionEnabled && this.activeKeys(); - } - - // Auth Params - - async setAccountAuthParams(authParams: any) { - this.accountAuthParams = authParams; - await Storage.get().setItem('auth_params', JSON.stringify(authParams)); - - if (this.offlineAuthParams && !this.offlineKeys) { - /* - This can happen if: - 1. You are signed into an account and have a local passcode - 2. You do an iCloud restore - 3. Your Keychain is wiped, but storage isn't, so offlineAuthParams still exists. - 4. You restore your account by signing in. At this point, no local passcode will actually be set. - The value of offlineAuthParams is stale. We want to delete it. - */ - - console.log('offlineAuthParams is stale, deleting'); - await Storage.get().removeItem(OfflineParamsKey); - } - } - - async setOfflineAuthParams(authParams: any) { - this.offlineAuthParams = authParams; - return Storage.get().setItem(OfflineParamsKey, JSON.stringify(authParams)); - } - - defaultProtocolVersionForKeys(keys: AccountKeys) { - if (keys && keys.ak) { - // If there's no version stored, and there's an ak, it has to be 002. Newer versions would have thier version stored in authParams. - return '002'; - } else { - return '001'; - } - } - - activeAuthParams() { - if (this.accountKeys) { - var params = this.accountAuthParams; - if (params && !params.version) { - params.version = this.defaultProtocolVersionForKeys(this.accountKeys); - } - return params; - } else if (this.offlineAuthParams) { - var params = this.offlineAuthParams; - if (params && !params.version) { - params.version = this.defaultProtocolVersionForKeys(this.offlineKeys); - } - return params; - } - } - - // User - - getUserEmail() { - return this.user && this.user.email; - } - - // Local Security - - async clearOfflineKeysAndData(force = false) { - // make sure user is authenticated before performing this step - if (this.offlineKeys && !this.offlineKeys.mk && !force) { - Alert.alert( - 'Unable to remove passcode. Make sure you are properly authenticated and try again.' - ); - return false; - } - this.offlineKeys = null; - this.offlineAuthParams = null; - await Storage.get().removeItem(OfflineParamsKey); - return this.persistKeys(); - } - - async persistOfflineKeys(keys: any) { - this.setOfflineKeys(keys); - if (!this.passcodeTiming) { - this.passcodeTiming = 'on-quit'; - } - return this.persistKeys(); - } - - async setOfflineKeys(keys: any) { - // offline keys are ephemeral and should not be stored anywhere - this.offlineKeys = keys; - - // Check to see if encryptedAccountKeys need decrypting - if (this.encryptedAccountKeys) { - // Decrypt and set - await protocolManager.itemTransformer.decryptItem( - this.encryptedAccountKeys, - this.offlineKeys - ); - // itemTransformer modifies in place. this.encryptedAccountKeys should now be decrypted - let decryptedKeys = new SFItem(this.encryptedAccountKeys); - if (decryptedKeys.errorDecrypting) { - console.error('Fatal: Error decrypting account keys'); - } else { - this.accountKeys = decryptedKeys.content.accountKeys; - this.encryptedAccountKeys = null; - await this.runPendingMigrations(); - } - } - } - - offlinePasscodeHash() { - return this.offlineKeys ? this.offlineKeys.pw : null; - } - - hasOfflinePasscode() { - return this.offlineKeys && this.offlineKeys.pw !== null; - } - - hasBiometrics() { - return this.biometricPrefs.enabled; - } - - async setPasscodeTiming(timing: string | null) { - this.passcodeTiming = timing; - return this.persistKeys(); - } - - async setBiometricsTiming(timing: { key: any }) { - this.biometricPrefs.timing = timing; - return this.saveBiometricPrefs(); - } - - async enableBiometrics() { - this.biometricPrefs.enabled = true; - if (!this.biometricPrefs.timing) { - this.biometricPrefs.timing = 'on-quit'; - } - return this.saveBiometricPrefs(); - } - - async disableBiometrics() { - this.biometricPrefs.enabled = false; - return this.saveBiometricPrefs(); - } - - async saveBiometricPrefs() { - return Storage.get().setItem( - BiometricsPrefs, - JSON.stringify(this.biometricPrefs) - ); - } - - getPasscodeTimingOptions() { - return [ - { - title: 'Immediately', - key: 'immediately', - selected: this.passcodeTiming === 'immediately', - }, - { - title: 'On Quit', - key: 'on-quit', - selected: this.passcodeTiming === 'on-quit', - }, - ]; - } - - getBiometricsTimingOptions() { - return [ - { - title: 'Immediately', - key: 'immediately', - selected: this.biometricPrefs.timing === 'immediately', - }, - { - title: 'On Quit', - key: 'on-quit', - selected: this.biometricPrefs.timing === 'on-quit', - }, - ]; - } - - static getDeviceBiometricsAvailability( - callback: (available: boolean, type?: BiometricsType, noun?: string) => void - ) { - if (__DEV__) { - const isAndroid = Platform.OS === 'android'; - if (isAndroid && Platform.Version < 23) { - callback(true, 'Fingerprint', 'Fingerprint (Dev)'); - } else if (isAndroid) { - callback(true, 'Biometrics', 'Biometrics (Dev)'); - } else { - callback(true, 'Face ID', 'Face ID'); - } - return; - } - FingerprintScanner.isSensorAvailable() - .then(type => { - const noun = type === 'Touch ID' ? 'Fingerprint' : type; - callback(true, type, noun); - }) - .catch(() => { - callback(false); - }); - } -} diff --git a/src/lib/reviewManager.ts b/src/lib/reviewManager.ts deleted file mode 100644 index 3a7db77a..00000000 --- a/src/lib/reviewManager.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Platform } from 'react-native'; -import * as StoreReview from 'react-native-store-review'; -import Storage from '@Lib/snjs/storageManager'; - -const RUN_COUNTS_BEFORE_REVIEW = [18, 45, 105]; - -export default class ReviewManager { - static initialize() { - if (Platform.OS === 'android' || !StoreReview.isAvailable) { - return; - } - - this.getRunCount().then(runCount => { - this.setRunCount(runCount + 1); - if (RUN_COUNTS_BEFORE_REVIEW.includes(runCount)) { - setTimeout(function () { - StoreReview.requestReview(); - }, 1000); - } - }); - } - - static async getRunCount() { - return Storage.get() - .getItem('runCount') - .then(runCount => { - if (runCount) { - return JSON.parse(runCount); - } - }); - } - - static async setRunCount(runCount: number) { - return Storage.get().setItem('runCount', JSON.stringify(runCount)); - } -} diff --git a/src/lib/reviewService.ts b/src/lib/reviewService.ts new file mode 100644 index 00000000..db4bce39 --- /dev/null +++ b/src/lib/reviewService.ts @@ -0,0 +1,35 @@ +import * as StoreReview from 'react-native-store-review'; +import { ApplicationService, Platform } from 'snjs'; + +const RUN_COUNTS_BEFORE_REVIEW = [18, 45, 105]; + +export class ReviewService extends ApplicationService { + async onAppLaunch() { + if ( + this.application?.platform === Platform.Android || + !StoreReview.isAvailable + ) { + return; + } + + const runCount = await this.getRunCount(); + this.setRunCount(runCount + 1); + if (RUN_COUNTS_BEFORE_REVIEW.includes(runCount)) { + setTimeout(function () { + StoreReview.requestReview(); + }, 1000); + } + } + + async getRunCount() { + return this.application?.getValue('runCount').then(runCount => { + if (runCount) { + return JSON.parse(runCount); + } + }); + } + + async setRunCount(runCount: number) { + return this.application?.setValue('runCount', JSON.stringify(runCount)); + } +} diff --git a/src/lib/snjs/alertManager.ts b/src/lib/snjs/alertManager.ts deleted file mode 100644 index 9bed1b26..00000000 --- a/src/lib/snjs/alertManager.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Alert } from 'react-native'; - -import { SFAlertManager } from 'snjs'; - -export default class AlertManager extends SFAlertManager { - private static instance: AlertManager; - - static get() { - if (!this.instance) { - this.instance = new AlertManager(); - } - - return this.instance; - } - - async alert(alertdata: { - title: string; - text: string; - closeButtonText?: string; - onClose?: () => void | Promise; - }) { - return new Promise(resolve => { - // On iOS, confirm should go first. On Android, cancel should go first. - let buttons = [ - { - text: alertdata.closeButtonText, - onPress: async () => { - alertdata.onClose && (await alertdata.onClose()); - resolve(); - }, - }, - ]; - Alert.alert(alertdata.title, alertdata.text, buttons, { - cancelable: true, - }); - }); - } - - async confirm(confirmData: { - title: string; - text?: string; - confirmButtonText?: string; - cancelButtonText?: string; - onConfirm?: () => void | Promise; - onCancel?: () => void | Promise; - onDismiss?: () => void | Promise; - }) { - return new Promise((resolve, reject) => { - // On iOS, confirm should go first. On Android, cancel should go first. - let buttons = [ - { - text: confirmData.cancelButtonText, - onPress: async () => { - confirmData.onCancel && (await confirmData.onCancel()); - reject(); - }, - }, - { - text: confirmData.confirmButtonText, - onPress: async () => { - confirmData.onConfirm && (await confirmData.onConfirm()); - resolve(); - }, - }, - ]; - Alert.alert(confirmData.title, confirmData.text, buttons, { - cancelable: true, - onDismiss: async () => { - confirmData.onDismiss && (await confirmData.onDismiss()); - reject(); - }, - }); - }); - } -} diff --git a/src/lib/snjs/authManager.ts b/src/lib/snjs/authManager.ts deleted file mode 100644 index 318d35f9..00000000 --- a/src/lib/snjs/authManager.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { Platform } from 'react-native'; -import _ from 'lodash'; -import { SFAuthManager, protocolManager } from 'snjs'; -import KeysManager from '@Lib/keysManager'; -import AlertManager from '@Lib/snjs/alertManager'; -import Server from '@Lib/snjs/httpManager'; -import Storage from '@Lib/snjs/storageManager'; - -export default class Auth extends SFAuthManager { - private static instance: Auth; - static get() { - if (!this.instance) { - // @ts-ignore underlying Authe manager handlers 3 agruments - this.instance = new Auth(Storage.get(), Server.get(), AlertManager.get()); - } - return this.instance; - } - - defaultServer() { - if (__DEV__) { - if (Platform.OS === 'android') { - return 'http://10.0.2.2:3000'; - } else { - return 'http://localhost:3000'; - } - } else { - return 'https://sync.standardnotes.org'; - } - } - - serverUrl() { - const user = KeysManager.get().user; - return (user && user.server) || this.defaultServer(); - } - - offline() { - const keys = KeysManager.get().activeKeys() || {}; - return !keys.jwt; - } - - async signout() { - await Storage.get().clearAllModels(); - await KeysManager.get().clearAccountKeysAndData(); - this._keys = null; - // DONT clear all data. We will do this ourselves manually, as we need to preserve certain data keys. - return super.signout(false); - } - - async keys() { - // AuthManager only handles account related keys. If we are requesting keys, - // we are referring to account keys. KeysManager.activeKeys can return local passcode - // keys. - if (this.offline()) { - return null; - } - - return KeysManager.get().activeKeys(); - } - - async getAuthParams() { - return KeysManager.get().activeAuthParams(); - } - - async handleAuthResponse( - response: { token: any }, - email: string, - url: string, - authParams: any, - keys: any - ) { - // We don't want to call super, as the super implementation is meant for web credentials - // super will save keys to storage, which we don't want. - // await super.handleAuthResponse(response, email, url, authParams, keys); - try { - this._keys = keys; - return Promise.all([ - KeysManager.get().persistAccountKeys( - _.merge(keys, { jwt: response.token }) - ), - KeysManager.get().setAccountAuthParams(authParams), - KeysManager.get().saveUser({ server: url, email: email }), - ]); - } catch (e) { - console.log('Error saving auth paramters', e); - return null; - } - } - - async verifyAccountPassword(password: string | undefined) { - const authParams = await this.getAuthParams(); - const keys = await protocolManager.computeEncryptionKeysForUser( - password, - authParams - ); - const success = keys.mk === (await this.keys()).mk; - return success; - } -} diff --git a/src/lib/snjs/httpManager.ts b/src/lib/snjs/httpManager.ts deleted file mode 100644 index 2735980c..00000000 --- a/src/lib/snjs/httpManager.ts +++ /dev/null @@ -1,23 +0,0 @@ -import KeysManager from '@Lib/keysManager'; - -import { SFHttpManager } from 'snjs'; - -export default class Server extends SFHttpManager { - private static instance: Server; - - static get() { - if (!this.instance) { - this.instance = new Server(); - } - - return this.instance; - } - - constructor() { - super(); - - this.setJWTRequestHandler(async () => { - return KeysManager.get().jwt(); - }); - } -} diff --git a/src/lib/snjs/migrationManager.ts b/src/lib/snjs/migrationManager.ts deleted file mode 100644 index fc74b729..00000000 --- a/src/lib/snjs/migrationManager.ts +++ /dev/null @@ -1,121 +0,0 @@ -import Auth from '@Lib/snjs/authManager'; -import ModelManager from '@Lib/snjs/modelManager'; -import PrivilegesManager from '@Lib/snjs/privilegesManager'; -import Storage from '@Lib/snjs/storageManager'; -import Sync from '@Lib/snjs/syncManager'; - -import { SFModelManager, SFMigrationManager } from 'snjs'; - -const base64 = require('base-64'); - -export default class MigrationManager extends SFMigrationManager { - private static instance: MigrationManager; - - static get() { - if (!this.instance) { - this.instance = new MigrationManager(); - } - - return this.instance; - } - - constructor() { - super(ModelManager.get(), Sync.get(), Storage.get(), Auth.get()); - } - - load() { - // Doesn't do anything, just gives consumer reason to call MigrationManager.get() so constructor - // can be run, which handles the actual loading - } - - registeredMigrations() { - return [this.downloadPrivileges(), this.downloadSmartTags()]; - } - - /* - Previously the mobile app only accepted a certain subset of item content_types before mapping - them from a sync request. SN|Privileges was not one of them, so now, we need to download all items - of a user to get at the privileges. - */ - - downloadPrivileges() { - let contentType = 'SN|Privileges'; - return { - name: 'dl-all-to-get-privs', - runOnlyOnce: true, - customHandler: async () => { - if (Auth.get().offline()) { - return; - } - - let options = { contentType: contentType }; - - // The user is signed in - Sync.get() - .stateless_downloadAllItems(options) - .then(async (items: any[]) => { - const matchingPrivs = items.filter(candidate => { - return candidate.content_type === contentType; - }); - - if (matchingPrivs.length === 0) { - return; - } - - const mapped = await ModelManager.get().mapResponseItemsToLocalModelsOmittingFields( - matchingPrivs, - null, - SFModelManager.MappingSourceRemoteRetrieved - ); - // Singleton manager usually resolves singletons on sync completion callback, - // but since we're manually mapping, we have to make it manually resolve singletons - PrivilegesManager.get().singletonManager.resolveSingletons(mapped); - }); - }, - }; - } - - downloadSmartTags() { - const contentType = 'SN|SmartTag'; - return { - name: 'dl-smart-tags', - runOnlyOnce: true, - customHandler: async () => { - if (Auth.get().offline()) { - return; - } - - const options = { contentType: contentType }; - - // The user is signed in - Sync.get() - .stateless_downloadAllItems(options) - .then(async (items: any[]) => { - let matchingTags = items.filter(candidate => { - return candidate.content_type === contentType; - }); - - if (matchingTags.length === 0) { - return; - } - - await ModelManager.get().mapResponseItemsToLocalModelsOmittingFields( - matchingTags, - null, - SFModelManager.MappingSourceRemoteRetrieved - ); - }); - }, - }; - } - - /* Overrides */ - - async encode(text: string) { - return base64.encode(text); - } - - async decode(base64String: string) { - return base64.decode(base64String); - } -} diff --git a/src/lib/snjs/modelManager.ts b/src/lib/snjs/modelManager.ts deleted file mode 100644 index 68eaa9af..00000000 --- a/src/lib/snjs/modelManager.ts +++ /dev/null @@ -1,369 +0,0 @@ -import { - SNMfa, - SNServerExtension, - SNExtension, - SNEditor, - SFPredicate, - SFPrivileges, - SFModelManager, - SNNote, - SNTag, - SNTheme, - SNComponent, - SNSmartTag, - SFItem as SNJSItem, -} from 'snjs'; -import _ from 'lodash'; -import Storage from '@Lib/snjs/storageManager'; -import '../../models/extend/item'; -import OptionsState from '@Lib/OptionsState'; - -type SFItem = typeof SNJSItem; - -SFModelManager.ContentTypeClassMapping = { - Note: SNNote, - Tag: SNTag, - 'SN|SmartTag': SNSmartTag, - Extension: SNExtension, - 'SN|Editor': SNEditor, - 'SN|Theme': SNTheme, - 'SN|Component': SNComponent, - 'SF|Extension': SNServerExtension, - 'SF|MFA': SNMfa, - 'SN|Privileges': SFPrivileges, -}; - -export default class ModelManager extends SFModelManager { - private static instance: ModelManager; - - static get() { - if (!this.instance) { - this.instance = new ModelManager(); - } - - return this.instance; - } - - constructor() { - super(); - - this.notes = []; - this.tags = []; - this.themes = []; - - this.buildSystemSmartTags(); - } - - handleSignout() { - super.handleSignout(); - this.notes.length = 0; - this.tags.length = 0; - this.themes.length = 0; - } - - addItems(items: SFItem, globalOnly = false) { - super.addItems(items, globalOnly); - - items.forEach((item: SFItem) => { - // In some cases, you just want to add the item to this.items, and not to the individual arrays - // This applies when you want to keep an item syncable, but not display it via the individual arrays - if (!globalOnly) { - if (item.content_type === 'Tag') { - if (!_.find(this.tags, { uuid: item.uuid })) { - this.tags.splice( - _.sortedIndexBy(this.tags, item, function (arrayItem) { - if (arrayItem.title) { - return arrayItem.title.toLowerCase(); - } else { - return ''; - } - }), - 0, - item - ); - } - } else if (item.content_type === 'Note') { - if (!_.find(this.notes, { uuid: item.uuid })) { - this.notes.unshift(item); - } - } else if (item.content_type === 'SN|Theme') { - if (!_.find(this.themes, { uuid: item.uuid })) { - this.themes.unshift(item); - } - } - } - }); - } - - async removeItemLocally(item: SFItem) { - await super.removeItemLocally(item); - - if (item.content_type === 'Tag') { - _.remove(this.tags, { uuid: item.uuid }); - } else if (item.content_type === 'Note') { - _.remove(this.notes, { uuid: item.uuid }); - } else if (item.content_type === 'SN|Theme') { - _.remove(this.themes, { uuid: item.uuid }); - } - - return Storage.get().deleteModel(item); - } - - noteCount() { - return this.notes.filter((n: { dummy: any }) => !n.dummy).length; - } - - /* Be sure not to use just findItems in your views, because those won't find system smart tags */ - getTagsWithIds(ids: string[]) { - let tagMatches = ModelManager.get().findItems(ids); - let smartMatches = this.getSmartTagsWithIds(ids); - return tagMatches.concat(smartMatches); - } - - getTagWithId(id: string) { - let tags = this.getTagsWithIds([id]); - if (tags.length > 0) { - return tags[0]; - } - } - - buildSystemSmartTags() { - this.systemSmartTags = SNSmartTag.systemSmartTags(); - } - - defaultSmartTag() { - return this.systemSmartTags[0]; - } - - systemSmartTagIds() { - return this.systemSmartTags.map((tag: { uuid: any }) => { - return tag.uuid; - }); - } - - getSmartTagWithId(id: string) { - return this.getSmartTags().find( - (candidate: { uuid: string }) => candidate.uuid === id - ); - } - - getSmartTagsWithIds(ids: string[]) { - return this.getSmartTags().filter((candidate: { uuid: string }) => - ids.includes(candidate.uuid) - ); - } - - getSmartTags() { - const userTags = this.validItemsForContentType('SN|SmartTag').sort( - ( - a: { content: { title: number } }, - b: { content: { title: number } } - ) => { - return a.content.title < b.content.title ? -1 : 1; - } - ); - return this.systemSmartTags.concat(userTags); - } - - trashSmartTag() { - return this.systemSmartTags.find( - (tag: { content: { isTrashTag: any } }) => tag.content.isTrashTag - ); - } - - trashedItems() { - return this.notesMatchingSmartTag(this.trashSmartTag()); - } - - emptyTrash() { - const notes = this.trashedItems(); - for (const note of notes) { - this.setItemToBeDeleted(note); - } - } - - notesMatchingSmartTag(tag: { content: { predicate: any; isTrashTag: any } }) { - const contentTypePredicate = new SFPredicate('content_type', '=', 'Note'); - const predicates = [contentTypePredicate, tag.content.predicate]; - if (!tag.content.isTrashTag) { - const notTrashedPredicate = new SFPredicate( - 'content.trashed', - '=', - false - ); - predicates.push(notTrashedPredicate); - } - return this.itemsMatchingPredicates(predicates); - } - - getNotes(options: OptionsState) { - let notes, - tags = [], - selectedSmartTag: { content: any }; - // if (options.selectedTagIds && options.selectedTagIds.length > 0 && options.selectedTagIds[0].key !== "all") { - let selectedTagIds = options.selectedTagIds; - if (selectedTagIds && selectedTagIds.length > 0) { - selectedSmartTag = - selectedTagIds.length === 1 && - this.getSmartTagWithId(selectedTagIds[0]); - if (selectedSmartTag) { - notes = this.notesMatchingSmartTag(selectedSmartTag); - } else { - tags = ModelManager.get().findItems(options.selectedTagIds); - if (tags.length > 0) { - let taggedNotes = new Set(); - for (const tag of tags) { - taggedNotes = new Set([...taggedNotes, ...new Set(tag.notes)]); - } - notes = Array.from(taggedNotes); - } - } - } - - if (!notes) { - notes = this.notes; - } - - let searchTerm = options.searchTerm; - if (searchTerm) { - searchTerm = searchTerm.toLowerCase(); - notes = notes.filter(function (note: { - safeTitle: () => { - (): any; - new (): any; - toLowerCase: { (): (string | undefined)[]; new (): any }; - }; - safeText: () => { - (): any; - new (): any; - toLowerCase: { (): (string | undefined)[]; new (): any }; - }; - }) { - return ( - note - .safeTitle() - .toLowerCase() - .includes(searchTerm ?? '') || - note - .safeText() - .toLowerCase() - .includes(searchTerm ?? '') - ); - }); - } - - const sortBy = options.sortBy; - const sortReverse = options.sortReverse; - - notes = notes.filter( - (note: { - deleted: any; - dummy: any; - content: { trashed: any }; - archived: any; - }) => { - if (note.deleted || note.dummy) { - return false; - } - - const isTrash = selectedSmartTag && selectedSmartTag.content.isTrashTag; - const canShowArchived = - (selectedSmartTag && selectedSmartTag.content.isArchiveTag) || - isTrash; - - if (!isTrash && note.content.trashed) { - return false; - } - - if (note.archived && !canShowArchived) { - return false; - } - - return true; - } - ); - - // @ts-ignore function invokes itself - const sortValueFn = ( - a: { [x: string]: string; pinned: any }, - b: { [x: string]: string; pinned: any }, - pinCheck = false - ) => { - if (!pinCheck) { - if (a.pinned && b.pinned) { - return sortValueFn(a, b, true); - } - if (a.pinned) { - return -1; - } - if (b.pinned) { - return 1; - } - } - let aValue = ''; - let bValue = ''; - if (sortBy) { - aValue = a[sortBy] || ''; - bValue = b[sortBy] || ''; - } - - let vector = 1; - - if (sortReverse) { - vector *= -1; - } - - if (sortBy === 'title') { - aValue = aValue.toLowerCase(); - bValue = bValue.toLowerCase(); - - if (aValue.length === 0 && bValue.length === 0) { - return 0; - } else if (aValue.length === 0 && bValue.length !== 0) { - return 1 * vector; - } else if (aValue.length !== 0 && bValue.length === 0) { - return -1 * vector; - } else { - vector *= -1; - } - } - - if (aValue > bValue) { - return -1 * vector; - } else if (aValue < bValue) { - return 1 * vector; - } - return 0; - }; - - notes = notes.sort(function ( - a: { [x: string]: string; pinned: any }, - b: { [x: string]: string; pinned: any } - ) { - return sortValueFn(a, b); - }); - - return { notes: notes, tags: tags }; - } - - /* - Misc - */ - humanReadableDisplayForContentType(contentType: string) { - // @ts-ignore cannot index this object trough uknown value - return { - Note: 'note', - Tag: 'tag', - 'SN|SmartTag': 'smart tag', - Extension: 'action-based extension', - 'SN|Component': 'component', - 'SN|Editor': 'editor', - 'SN|Theme': 'theme', - 'SF|Extension': 'server extension', - 'SF|MFA': 'two-factor authentication setting', - 'SN|FileSafe|Credentials': 'FileSafe credential', - 'SN|FileSafe|FileMetadata': 'FileSafe file', - 'SN|FileSafe|Integration': 'FileSafe integration', - }[contentType]; - } -} diff --git a/src/lib/snjs/privilegesManager.ts b/src/lib/snjs/privilegesManager.ts deleted file mode 100644 index 1e4ae3ed..00000000 --- a/src/lib/snjs/privilegesManager.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { Platform } from 'react-native'; -import { SFPrivilegesManager, SFSingletonManager } from 'snjs'; -import KeysManager from '@Lib/keysManager'; -import { SCREEN_AUTHENTICATE } from '@Screens/screens'; -import AuthenticationSourceAccountPassword from '@Screens/Authentication/Sources/AuthenticationSourceAccountPassword'; -import AuthenticationSourceBiometric from '@Screens/Authentication/Sources/AuthenticationSourceBiometric'; -import AuthenticationSourceLocalPasscode from '@Screens/Authentication/Sources/AuthenticationSourceLocalPasscode'; -import Auth from '@Lib/snjs/authManager'; -import ModelManager from '@Lib/snjs/modelManager'; -import Sync from '@Lib/snjs/syncManager'; -import Storage from '@Lib/snjs/storageManager'; -import { ICON_CLOSE } from '@Style/icons'; -import StyleKit from '@Style/StyleKit'; - -export default class PrivilegesManager extends SFPrivilegesManager { - private static instance: PrivilegesManager; - - static get() { - if (!this.instance) { - let singletonManager = new SFSingletonManager( - ModelManager.get(), - Sync.get() - ); - this.instance = new PrivilegesManager( - ModelManager.get(), - Sync.get(), - singletonManager - ); - } - - return this.instance; - } - - constructor( - modelManager: ModelManager, - syncManager: Sync, - singletonManager: any - ) { - super(modelManager, syncManager, singletonManager); - - this.setDelegate({ - isOffline: async () => { - return Auth.get().offline(); - }, - hasLocalPasscode: async () => { - const hasPasscode = KeysManager.get().hasOfflinePasscode(); - const hasBiometrics = KeysManager.get().hasBiometrics(); - return hasPasscode || hasBiometrics; - }, - saveToStorage: async (key: string, value: string | null | undefined) => { - return Storage.get().setItem(key, value); - }, - getFromStorage: async (key: string) => { - return Storage.get().getItem(key); - }, - }); - } - - async presentPrivilegesModal( - action: any, - navigation: { - navigate: ( - arg0: string, - arg1: { - leftButton: { title: string | null; iconName: string | null }; - authenticationSources: any[]; - hasCancelOption: boolean; - sessionLengthOptions: any; - selectedSessionLength: any; - onSuccess: (selectedSessionLength: any) => void; - onCancel: () => void; - } - ) => void; - }, - onSuccess: { (): void; (): any }, - onCancel?: () => any - ) { - if (this.authenticationInProgress()) { - onCancel && onCancel(); - return; - } - - const customSuccess = () => { - onSuccess && onSuccess(); - this.authInProgress = false; - }; - - const customCancel = () => { - onCancel && onCancel(); - this.authInProgress = false; - }; - - const sources = await this.sourcesForAction(action); - - const sessionLengthOptions = await this.getSessionLengthOptions(); - const selectedSessionLength = await this.getSelectedSessionLength(); - - navigation.navigate(SCREEN_AUTHENTICATE, { - leftButton: { - title: Platform.OS === 'ios' ? 'Cancel' : null, - iconName: - Platform.OS === 'ios' ? null : StyleKit.nameForIcon(ICON_CLOSE), - }, - authenticationSources: sources, - hasCancelOption: true, - sessionLengthOptions: sessionLengthOptions, - selectedSessionLength: selectedSessionLength, - onSuccess: (selectedSessionLength: any) => { - this.setSessionLength(selectedSessionLength); - customSuccess(); - }, - onCancel: () => { - customCancel(); - }, - }); - - this.authInProgress = true; - } - - authenticationInProgress() { - return this.authInProgress; - } - - async sourcesForAction(action: any) { - const sourcesForCredential = (credential: any) => { - if (credential === SFPrivilegesManager.CredentialAccountPassword) { - return [new AuthenticationSourceAccountPassword()]; - } else if (credential === SFPrivilegesManager.CredentialLocalPasscode) { - const hasPasscode = KeysManager.get().hasOfflinePasscode(); - const hasBiometrics = KeysManager.get().hasBiometrics(); - let sources = []; - if (hasPasscode) { - sources.push(new AuthenticationSourceLocalPasscode()); - } - if (hasBiometrics) { - sources.push(new AuthenticationSourceBiometric()); - } - return sources; - } - }; - - const credentials = await this.netCredentialsForAction(action); - let sources: any[] = []; - for (const credential of credentials) { - sources = sources - .concat(sourcesForCredential(credential)) - .sort((a, b) => { - return a.sort - b.sort; - }); - } - - return sources; - } - - async grossCredentialsForAction(action: any) { - const privs = await this.getPrivileges(); - const creds = privs.getCredentialsForAction(action); - return creds; - } -} diff --git a/src/lib/snjs/storageManager.ts b/src/lib/snjs/storageManager.ts deleted file mode 100644 index 145eece1..00000000 --- a/src/lib/snjs/storageManager.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { Platform } from 'react-native'; -import { SFStorageManager, SFItem as SNJSItem } from 'snjs'; -import AsyncStorage from '@react-native-community/async-storage'; -import AlertManager from '@Lib/snjs/alertManager'; -import { isNullOrUndefined } from '@Lib/utils'; - -type SFItem = typeof SNJSItem; - -export default class Storage extends SFStorageManager { - private static instance: Storage; - - static get() { - if (!this.instance) { - this.instance = new Storage(); - } - - return this.instance; - } - - constructor() { - super(); - this.isAndroid = Platform.OS === 'android'; - this.platformString = this.isAndroid ? 'Android' : 'iOS'; - } - - async getItem(key: string) { - try { - return AsyncStorage.getItem(key); - } catch (error) { - console.log('Error getting item', error); - return null; - } - } - - async getMultiItems(keys: string[]) { - return AsyncStorage.multiGet(keys).then(stores => { - const items: Record = {}; - stores.map((_result, i, store) => { - let key = store[i][0]; - let value = store[i][1]; - items[key] = value; - }); - return items; - }); - } - - async setItem(key: string, value: string | undefined | null) { - if (value === null || value === undefined || isNullOrUndefined(key)) { - return; - } - try { - return AsyncStorage.setItem(key, value); - } catch (error) { - console.log('Error setting item', error); - return null; - } - } - - async removeItem(key: string) { - return AsyncStorage.removeItem(key); - } - - async clearKeys(keys: string[]) { - return AsyncStorage.multiRemove(keys); - } - - async clear() { - return AsyncStorage.clear(); - } - - // Models - async getAllModels() { - const itemsFromStores = (stores: any[]) => { - const items: any[] = []; - stores.map((_result, i, store) => { - // const key = store[i][0]; - const value = store[i][1]; - if (value) { - items.push(JSON.parse(value)); - } - }); - - return items; - }; - - /* - As of react-native-asyncstorage 1.4.0: - - If Android has items saved that are over ~1MB, then: - - let stores = await AsyncStorage.multiGet(keys); - items = items.concat(itemsFromStores(stores)); - - will fail silently and no items will load. - - let item = await AsyncStorage.getItem(key); - items.push(JSON.parse(item)); - - will fail with an exception 'Cursor Window: Window is full'. - But actually if you wrap it in a try catch, then it will throw an exception correctly. - So that's what we're using now on Android. - - let item = itemsFromStores(await AsyncStorage.multiGet([key]))[0]; - items.push(item); - - will succeed completely. - - Issue created here: https://github.com/react-native-community/react-native-async-storage/issues/105 - - So what we'll do for now is if Android, use multiGet with just 1 key each time. - We need to determine why multiGet([key]) works, but getItem(key) doesn't. - - It looks like the reason getItem fails while multiGet doesn't is because getItem - correctly returns the exception. However, even when there is an exception, getItem - internally still retrieves the item value. So the value and exception are both present, - but only the exception is sent. - - Whereas with multiGet, exceptions aren't reported at all, so the value is sent up. - When multiGet gets patched to reports errors, I suspect this loophole will no longer work. - - However, getItem's `callback` param can be used instead of the promise, which will return both a value and an exception, - and we can choose which one we want to handle. - - multiGet also currently totally fails if even 1 key fails: - https://github.com/react-native-community/react-native-async-storage/issues/106 - */ - - const keys = await this.getAllModelKeys(); - let items: any[] = []; - const failedItemIds = []; - if (this.isAndroid) { - for (const key of keys) { - try { - const item = await AsyncStorage.getItem(key); - if (item) { - items.push(JSON.parse(item)); - } - } catch (e) { - const id = key.replace('Item-', ''); - failedItemIds.push(id); - console.log('Error getting item', key, e); - } - } - } else { - try { - let stores = await AsyncStorage.multiGet(keys); - items = items.concat(itemsFromStores(stores)); - } catch (e) { - console.log('Error getting items', keys, e); - } - } - - if (failedItemIds.length > 0) { - this.showLoadFailForItemIds(failedItemIds); - } - - return items; - } - - showLoadFailForItemIds(failedItemIds: string[]) { - let text = `The following items could not be loaded. This may happen if you are in low-memory conditions, or if the note is very large in size. For compatibility with ${this.platformString}, we recommend breaking up large notes into smaller chunks using the desktop or web app.\n\nItems:\n`; - let index = 0; - text += failedItemIds.map(id => { - let result = id; - if (index !== failedItemIds.length - 1) { - result += '\n'; - } - index++; - return result; - }); - AlertManager.get().alert({ title: 'Unable to load item', text: text }); - } - - keyForItem(item: SFItem) { - return 'Item-' + item.uuid; - } - - async getAllModelKeys() { - const keys = await AsyncStorage.getAllKeys(); - const filtered = keys.filter(key => { - return key.includes('Item-'); - }); - return filtered; - } - - // TODO: Not sure about this - // @ts-ignore - async saveModel(item: SFItem) { - return this.saveModel([item]); - } - - /* - // Note: multiset is not working properly; returns with error - AsyncStorage.multiSet(data, function(error){ callback(); }) - Each item is saved individually. - */ - async saveModels(items?: SFItem[]) { - if (!items || items.length === 0) { - return; - } - - return Promise.all( - items.map(item => { - return AsyncStorage.setItem( - this.keyForItem(item), - JSON.stringify(item) - ); - }) - ); - } - - async deleteModel(item: SFItem) { - return AsyncStorage.removeItem(this.keyForItem(item)); - } - - async clearAllModels() { - const itemKeys = await this.getAllModelKeys(); - return AsyncStorage.multiRemove(itemKeys); - } -} diff --git a/src/lib/snjs/syncManager.ts b/src/lib/snjs/syncManager.ts deleted file mode 100644 index 95499043..00000000 --- a/src/lib/snjs/syncManager.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { SFSyncManager } from 'snjs'; -import KeysManager from '@Lib/keysManager'; -import Auth from '@Lib/snjs/authManager'; -import Server from '@Lib/snjs/httpManager'; -import ModelManager from '@Lib/snjs/modelManager'; -import Storage from '@Lib/snjs/storageManager'; - -export default class Sync extends SFSyncManager { - private static instance: Sync; - - static get() { - if (!this.instance) { - this.instance = new Sync(); - } - - return this.instance; - } - - constructor() { - super(ModelManager.get(), Storage.get(), Server.get()); - KeysManager.get().registerAccountRelatedStorageKeys([ - 'syncToken', - 'cursorToken', - ]); - - this.setKeyRequestHandler((request: any) => { - let keys; - if ( - request === SFSyncManager.KeyRequestLoadSaveAccount || - request === SFSyncManager.KeyRequestLoadLocal - ) { - keys = KeysManager.get().activeKeys(); - } else if (request === SFSyncManager.KeyRequestSaveLocal) { - // Only return keys when saving local if storage encryption is enabled. - keys = - KeysManager.get().isStorageEncryptionEnabled() && - KeysManager.get().activeKeys(); - } - - const auth_params = KeysManager.get().activeAuthParams(); - const offline = Auth.get().offline(); - - return { keys, auth_params, offline }; - }); - - // Content types appearing first are always mapped first - this.contentTypeLoadPriority = [ - 'SN|UserPreferences', - 'SN|Privileges', - 'SN|Component', - 'SN|Theme', - ]; - } - - async resaveOfflineData() { - const items = ModelManager.get().allItems; - return this.writeItemsToLocalStorage(items, false); - } - - async getServerURL() { - return Auth.get().serverUrl(); - } - - handleSignout() { - super.handleSignout(); - - // we might need to trigger this via events - // this.syncObservers.forEach(function(mapping){ - // mapping.callback(); - // }) - } -} diff --git a/src/lib/userPrefsManager.ts b/src/lib/userPrefsManager.ts deleted file mode 100644 index bfb285f4..00000000 --- a/src/lib/userPrefsManager.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { dateFromJsonString, isNullOrUndefined } from '@Lib/utils'; -import Storage from '@Lib/snjs/storageManager'; - -export const LAST_EXPORT_DATE_KEY = 'LastExportDateKey'; -export const DONT_SHOW_AGAIN_UNSUPPORTED_EDITORS_KEY = - 'DoNotShowAgainUnsupportedEditorsKey'; - -export default class UserPrefsManager { - private static instance: UserPrefsManager; - data: Record; - static get() { - if (!this.instance) { - this.instance = new UserPrefsManager(); - } - return this.instance; - } - - constructor() { - this.data = {}; - } - - async clearPref({ key }: { key: string }) { - this.data[key] = null; - return Storage.get().clearKeys([key]); - } - - async setPref({ key, value }: { key: string; value: unknown }) { - await Storage.get().setItem(key, JSON.stringify(value)); - this.data[key] = value; - } - - async getPref({ key }: { key: string }) { - if (isNullOrUndefined(this.data[key])) { - const item = await Storage.get().getItem(key); - this.data[key] = JSON.parse(item ?? ''); - } - - return this.data[key]; - } - - async getPrefAsDate({ key }: { key: string }) { - if (isNullOrUndefined(this.data[key])) { - const item = await Storage.get().getItem(key); - this.data[key] = dateFromJsonString(item ?? ''); - } - - return this.data[key]; - } - - async isPrefSet({ key }: { key: string }) { - return (await this.getPref({ key: key })) !== null; - } - - async isPrefEqualTo({ key, value }: { key: string; value: unknown }) { - return (await this.getPref({ key: key })) === value; - } -} diff --git a/src/screens/Abstract.tsx b/src/screens/Abstract.tsx index 3505d605..b469953f 100644 --- a/src/screens/Abstract.tsx +++ b/src/screens/Abstract.tsx @@ -8,7 +8,6 @@ import Icon from 'react-native-vector-icons/Ionicons'; import HeaderTitleView from '@Components/HeaderTitleView'; import ThemedComponent from '@Components/ThemedComponent'; import ApplicationState, { AppStateType } from '@Lib/ApplicationState'; -import PrivilegesManager from '@Lib/snjs/privilegesManager'; import StyleKit from '@Style/StyleKit'; const IoniconsHeaderButton = (passMeFurther: HeaderButtonProps) => ( diff --git a/src/screens/ComponentView.tsx b/src/screens/ComponentView.tsx index b4e1d77b..8c78a17b 100644 --- a/src/screens/ComponentView.tsx +++ b/src/screens/ComponentView.tsx @@ -9,12 +9,6 @@ import { } from 'react-native'; import Icon from 'react-native-vector-icons/Ionicons'; import { WebView } from 'react-native-webview'; -import ApplicationState from '@Lib/ApplicationState'; -import ComponentManager from '@Lib/componentManager'; -import ModelManager from '@Lib/snjs/modelManager'; -import UserPrefsManager, { - DONT_SHOW_AGAIN_UNSUPPORTED_EDITORS_KEY, -} from '@Lib/userPrefsManager'; import { ICON_LOCK } from '@Style/icons'; import StyleKit from '@Style/StyleKit'; diff --git a/src/screens/KeyRecovery.tsx b/src/screens/KeyRecovery.tsx index 09c2ed79..68b44b0b 100644 --- a/src/screens/KeyRecovery.tsx +++ b/src/screens/KeyRecovery.tsx @@ -6,11 +6,7 @@ import { SFModelManager, protocolManager } from 'snjs'; import SectionedTableCell from '@Components/SectionedTableCell'; import TableSection from '@Components/TableSection'; import ApplicationState from '@Lib/ApplicationState'; -import KeysManager from '@Lib/keysManager'; import Abstract, { AbstractProps, AbstractState } from '@Screens/Abstract'; -import ModelManager from '@Lib/snjs/modelManager'; -import Sync from '@Lib/snjs/syncManager'; -import AlertManager from '@Lib/snjs/alertManager'; import { ICON_CLOSE } from '@Style/icons'; import StyleKit from '@Style/StyleKit'; diff --git a/src/screens/ManagePrivileges.tsx b/src/screens/ManagePrivileges.tsx index f00a8161..4c862731 100644 --- a/src/screens/ManagePrivileges.tsx +++ b/src/screens/ManagePrivileges.tsx @@ -6,10 +6,7 @@ import SectionHeader from '@Components/SectionHeader'; import SectionedAccessoryTableCell from '@Components/SectionedAccessoryTableCell'; import SectionedTableCell from '@Components/SectionedTableCell'; import LockedView from '@Containers/LockedView'; -import ApplicationState from '@Lib/ApplicationState'; -import KeysManager from '@Lib/keysManager'; -import Auth from '@Lib/snjs/authManager'; -import PrivilegesManager from '@Lib/snjs/privilegesManager'; +import { ApplicationState } from '@Lib/ApplicationState'; import Abstract, { AbstractState, AbstractProps } from '@Screens/Abstract'; import { ICON_CHECKMARK } from '@Style/icons'; import StyleKit from '@Style/StyleKit'; diff --git a/src/screens/Root.tsx b/src/screens/Root.tsx index c24655ba..d9735e0e 100644 --- a/src/screens/Root.tsx +++ b/src/screens/Root.tsx @@ -9,15 +9,10 @@ import ApplicationState, { NoteSideMenuToggleChange, AppStateType, } from '@Lib/ApplicationState'; -import KeysManager from '@Lib/keysManager'; import Abstract, { AbstractProps, AbstractState } from '@Screens/Abstract'; import Compose from '@Screens/Compose'; import Notes from '@Screens/Notes/Notes'; import { SCREEN_AUTHENTICATE } from '@Screens/screens'; -import AlertManager from '@Lib/snjs/alertManager'; -import Auth from '@Lib/snjs/authManager'; -import ModelManager from '@Lib/snjs/modelManager'; -import Sync from '@Lib/snjs/syncManager'; import StyleKit from '@Style/StyleKit'; import { hexToRGBA } from '@Style/utils'; import AuthenticationSource from './Authentication/Sources/AuthenticationSource'; diff --git a/src/screens/Settings/Sections/OptionsSection.tsx b/src/screens/Settings/Sections/OptionsSection.tsx index 491b10a0..d4ea3b4a 100644 --- a/src/screens/Settings/Sections/OptionsSection.tsx +++ b/src/screens/Settings/Sections/OptionsSection.tsx @@ -6,15 +6,10 @@ import SectionHeader from '@Components/SectionHeader'; import SectionedAccessoryTableCell from '@Components/SectionedAccessoryTableCell'; import SectionedOptionsTableCell from '@Components/SectionedOptionsTableCell'; import TableSection from '@Components/TableSection'; -import BackupsManager from '@Lib/BackupsManager'; -import KeysManager from '@Lib/keysManager'; import moment from '@Lib/moment'; import UserPrefsManager, { LAST_EXPORT_DATE_KEY } from '@Lib/userPrefsManager'; -import Auth from '@Lib/snjs/authManager'; import Abstract, { AbstractProps, AbstractState } from '@Screens/Abstract'; -import { SFPrivilegesManager } from 'snjs'; - type Props = { title: string; onSignOutPress: () => Promise; diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx index f63aff77..664ae22f 100644 --- a/src/screens/Settings/Settings.tsx +++ b/src/screens/Settings/Settings.tsx @@ -8,7 +8,7 @@ import TableSection from '@Components/TableSection'; import LockedView from '@Containers/LockedView'; import ApplicationState from '@Lib/ApplicationState'; import KeysManager from '@Lib/keysManager'; -import AlertManager from '@Lib/snjs/alertManager'; +import AlertManager from '@Lib/AlertService'; import Auth from '@Lib/snjs/authManager'; import Storage from '@Lib/snjs/storageManager'; import Sync from '@Lib/snjs/syncManager'; diff --git a/src/screens/SideMenu/MainSideMenu.tsx b/src/screens/SideMenu/MainSideMenu.tsx index ccb36f38..c87d0d32 100644 --- a/src/screens/SideMenu/MainSideMenu.tsx +++ b/src/screens/SideMenu/MainSideMenu.tsx @@ -12,7 +12,7 @@ import SideMenuHero from '@Screens/SideMenu/SideMenuHero'; import SideMenuManager from '@Screens/SideMenu/SideMenuManager'; import SideMenuSection from '@Screens/SideMenu/SideMenuSection'; import TagSelectionList from '@Screens/SideMenu/TagSelectionList'; -import AlertManager from '@Lib/snjs/alertManager'; +import AlertManager from '@Lib/AlertService'; import Auth from '@Lib/snjs/authManager'; import Sync from '@Lib/snjs/syncManager'; import { ICON_BRUSH, ICON_SETTINGS } from '@Style/icons'; @@ -21,12 +21,10 @@ import StyleKit from '@Style/StyleKit'; import ThemeManager from '@Style/ThemeManager'; import { LIGHT_MODE_KEY, DARK_MODE_KEY } from '@Style/utils'; -import { SFAuthManager, SNTheme as SNJSTheme } from 'snjs'; +import { SFAuthManager, SNTheme } from 'snjs'; import { Mode } from 'react-native-dark-mode'; import { AbstractProps, AbstractState } from '@Screens/Abstract'; -type SNTheme = typeof SNJSTheme; - type State = { outOfSync: boolean; actionSheet: JSX.Element | null; diff --git a/src/style/themesManager.ts b/src/style/themesManager.ts new file mode 100644 index 00000000..ce148c75 --- /dev/null +++ b/src/style/themesManager.ts @@ -0,0 +1,187 @@ +import { WebApplication } from '@/ui_models/application'; +import _ from 'lodash'; +import { + StorageValueModes, + EncryptionIntent, + ApplicationService, + SNTheme, + ComponentArea, + removeFromArray, + ApplicationEvent, +} from 'snjs'; +import { AppStateEvent } from '@/ui_models/app_state'; + +const CACHED_THEMES_KEY = 'cachedThemes'; + +export class ThemeManager extends ApplicationService { + private activeThemes: string[] = []; + private unsubState!: () => void; + private unregisterDesktop!: () => void; + private unregisterComponent!: () => void; + + /** @override */ + async onAppLaunch() { + super.onAppLaunch(); + this.unsubState = this.webApplication + .getAppState() + .addObserver(async eventName => { + if (eventName === AppStateEvent.DesktopExtsReady) { + this.activateCachedThemes(); + } + }); + } + + onAppEvent(event: ApplicationEvent) { + super.onAppEvent(event); + if (event === ApplicationEvent.SignedOut) { + this.deactivateAllThemes(); + } + } + + get webApplication() { + return this.application as WebApplication; + } + + deinit() { + this.unsubState(); + (this.unsubState as any) = undefined; + this.activeThemes.length = 0; + this.unregisterDesktop(); + this.unregisterComponent(); + (this.unregisterDesktop as any) = undefined; + (this.unregisterComponent as any) = undefined; + super.deinit(); + } + + /** @override */ + async onAppStart() { + super.onAppStart(); + this.registerObservers(); + if (!this.webApplication.getDesktopService().isDesktop) { + this.activateCachedThemes(); + } + } + + private async activateCachedThemes() { + const cachedThemes = await this.getCachedThemes(); + const writeToCache = false; + for (const theme of cachedThemes) { + this.activateTheme(theme, writeToCache); + } + } + + private registerObservers() { + this.unregisterDesktop = this.webApplication + .getDesktopService() + .registerUpdateObserver(component => { + if (component.active && component.isTheme()) { + this.deactivateTheme(component.uuid); + setTimeout(() => { + this.activateTheme(component as SNTheme); + }, 10); + } + }); + + this.unregisterComponent = this.application!.componentManager!.registerHandler( + { + identifier: 'themeManager', + areas: [ComponentArea.Themes], + activationHandler: component => { + if (component.active) { + this.activateTheme(component as SNTheme); + } else { + this.deactivateTheme(component.uuid); + } + }, + } + ); + } + + private deactivateAllThemes() { + for (const uuid of this.activeThemes) { + this.deactivateTheme(uuid, false); + } + this.activeThemes = []; + this.decacheThemes(); + } + + private activateTheme(theme: SNTheme, writeToCache = true) { + if (this.activeThemes.find(uuid => uuid === theme.uuid)) { + return; + } + console.log(theme.hosted_url); + this.activeThemes.push(theme.uuid); + const url = this.application!.componentManager!.urlForComponent(theme)!; + const link = document.createElement('link'); + console.log(url); + link.href = url; + link.type = 'text/css'; + link.rel = 'stylesheet'; + link.media = 'screen,print'; + link.id = theme.uuid; + document.getElementsByTagName('head')[0].appendChild(link); + if (writeToCache) { + this.cacheThemes(); + } + } + + private deactivateTheme(uuid: string, recache = true) { + const element = document.getElementById(uuid) as HTMLLinkElement; + if (element) { + element.disabled = true; + element.parentNode!.removeChild(element); + } + removeFromArray(this.activeThemes, uuid); + if (recache) { + this.cacheThemes(); + } + } + + private async cacheThemes() { + const themes = this.application!.getAll(this.activeThemes) as SNTheme[]; + const mapped = await Promise.all( + themes.map(async theme => { + const payload = theme.payloadRepresentation(); + const processedPayload = await this.application!.protocolService!.payloadByEncryptingPayload( + payload, + EncryptionIntent.LocalStorageDecrypted + ); + return processedPayload; + }) + ); + return this.application!.setValue( + CACHED_THEMES_KEY, + mapped, + StorageValueModes.Nonwrapped + ); + } + + private async decacheThemes() { + if (this.application) { + return this.application.removeValue( + CACHED_THEMES_KEY, + StorageValueModes.Nonwrapped + ); + } + } + + private async getCachedThemes() { + const cachedThemes = await this.application!.getValue( + CACHED_THEMES_KEY, + StorageValueModes.Nonwrapped + ); + if (cachedThemes) { + const themes = []; + for (const cachedTheme of cachedThemes) { + const payload = this.application!.createPayloadFromObject(cachedTheme); + const theme = this.application!.createItemFromPayload( + payload + ) as SNTheme; + themes.push(theme); + } + return themes; + } else { + return []; + } + } +} diff --git a/src/style/utils.ts b/src/style/utils.ts index 2bd93c7c..350effa5 100644 --- a/src/style/utils.ts +++ b/src/style/utils.ts @@ -1,12 +1,10 @@ -import { SNTheme as SNJSTheme } from 'snjs'; +import { SNTheme } from 'snjs'; /* eslint-disable no-bitwise */ export const LIGHT_MODE_KEY = 'light'; export const DARK_MODE_KEY = 'dark'; export const LIGHT_CONTENT = 'light-content'; export const DARK_CONTENT = 'dark-content'; -type SNTheme = typeof SNJSTheme; - export function statusBarColorForTheme(theme: SNTheme) { // The main nav bar uses contrast background color if (!theme.luminosity) { diff --git a/src/types/snjs/index.d.ts b/src/types/snjs/index.d.ts deleted file mode 100644 index 249fd224..00000000 --- a/src/types/snjs/index.d.ts +++ /dev/null @@ -1,37 +0,0 @@ -declare module 'snjs' { - export const SNTheme: any; - export const SNProtocolManager: any; - export const protocolManager: any; - export const SFItem: any; - export const SFItemParams: any; - export const SFPredicate: any; - export const SNNote: any; - export const SNTag: any; - export const SNSmartTag: any; - export const SNMfa: any; - export const SNServerExtension: any; - export const SNComponent: any; - export const SNEditor: any; - export const Action: any; - export const SNExtension: any; - export const SNEncryptedStorage: any; - export const SFHistorySession: any; - export const SFItemHistory: any; - export const SFPrivileges: any; - export const SNWebCrypto: any; - export const SNCryptoJS: any; - export const SNReactNativeCrypto: any; - export const findInArray: any; - export const SFModelManager: any; - export const SFHttpManager: any; - export const SFStorageManager: any; - export const SFSyncManager: any; - export const SFAuthManager: any; - export const SFAlertManager: any; - export const SFSessionHistoryManager: any; - export const SFPrivilegesManager: any; - export const SFSingletonManager: any; - export const SNEncryptedStorage: any; - export const SNComponentManager: any; - export const SFMigrationManager: any; -} diff --git a/yarn.lock b/yarn.lock index 8357d36e..5bedbc05 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1004,10 +1004,12 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" -"@react-native-community/async-storage@1.6.3": - version "1.6.3" - resolved "https://registry.yarnpkg.com/@react-native-community/async-storage/-/async-storage-1.6.3.tgz#1a713e8c5cacd543ab8539080a5ce57ad917da88" - integrity sha512-67K2akX90uc252zKMYJt1wISvaEH6ARtdTI9bUkwmOFXVPyVk1DfPnaRmyUzcVdeCBOO1n0xv9YO2GSppIormQ== +"@react-native-community/async-storage@1.10.1": + version "1.10.1" + resolved "https://registry.yarnpkg.com/@react-native-community/async-storage/-/async-storage-1.10.1.tgz#23902625cf146a25b2929d72558caa437c72c4a8" + integrity sha512-9wjW4lNAKkNgeQtj4dG2graKoJoiAELvh6QDXZ55ld5J58kOfJIe6ELaGBUtIAoXPpmjsddjpXrfV6af3mN2JA== + dependencies: + deep-assign "^3.0.0" "@react-native-community/cli-debugger-ui@^4.7.0": version "4.7.0" @@ -1301,13 +1303,20 @@ "@types/react" "*" "@types/react-native" "*" -"@types/react-native@*", "@types/react-native@^0.62.7": +"@types/react-native@*": version "0.62.7" resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.62.7.tgz#bfc5ed03ba576f288603daa3f67f0f67d9a8bf57" integrity sha512-FGFEt9GcFVl//XxWmxkeBxAx0YnzyEhJpR8hOJrjfaFKZm0KjHzzyCmCksBAP2qHSTrcJCiBkIvYCX/kGiOgww== dependencies: "@types/react" "*" +"@types/react-native@^0.62.8": + version "0.62.8" + resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.62.8.tgz#224602561f75b838ed6e3b5ea37093bb84cffd74" + integrity sha512-YEf0tH3xYJpQB12Vvzoy7cqPY3mvbbulTnG3G7ToKOuJuqigAN3K9NNAaxNAAm1zCj+UtObhzcaJtPRwX1z6Fw== + dependencies: + "@types/react" "*" + "@types/react@*": version "16.9.34" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.34.tgz#f7d5e331c468f53affed17a8a4d488cd44ea9349" @@ -1340,7 +1349,7 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@^2.25.0", "@typescript-eslint/eslint-plugin@^2.31.0": +"@typescript-eslint/eslint-plugin@^2.25.0": version "2.31.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.31.0.tgz#942c921fec5e200b79593c71fafb1e3f57aa2e36" integrity sha512-iIC0Pb8qDaoit+m80Ln/aaeu9zKQdOLF4SHcGLarSeY1gurW6aU4JsOPMjKQwXlw70MvWKZQc6S2NamA8SJ/gg== @@ -1350,6 +1359,16 @@ regexpp "^3.0.0" tsutils "^3.17.1" +"@typescript-eslint/eslint-plugin@^2.33.0": + version "2.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.33.0.tgz#d6c8319d5011b4783bb3d2dadf105d8bdd499bd5" + integrity sha512-QV6P32Btu1sCI/kTqjTNI/8OpCYyvlGjW5vD8MpTIg+HGE5S88HtT1G+880M4bXlvXj/NjsJJG0aGcVh0DdbeQ== + dependencies: + "@typescript-eslint/experimental-utils" "2.33.0" + functional-red-black-tree "^1.0.1" + regexpp "^3.0.0" + tsutils "^3.17.1" + "@typescript-eslint/experimental-utils@2.31.0": version "2.31.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.31.0.tgz#a9ec514bf7fd5e5e82bc10dcb6a86d58baae9508" @@ -1360,7 +1379,17 @@ eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/parser@^2.25.0", "@typescript-eslint/parser@^2.31.0": +"@typescript-eslint/experimental-utils@2.33.0": + version "2.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.33.0.tgz#000f1e5f344fbea1323dc91cc174805d75f99a03" + integrity sha512-qzPM2AuxtMrRq78LwyZa8Qn6gcY8obkIrBs1ehqmQADwkYzTE1Pb4y2W+U3rE/iFkSWcWHG2LS6MJfj6SmHApg== + dependencies: + "@types/json-schema" "^7.0.3" + "@typescript-eslint/typescript-estree" "2.33.0" + eslint-scope "^5.0.0" + eslint-utils "^2.0.0" + +"@typescript-eslint/parser@^2.25.0": version "2.31.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.31.0.tgz#beddd4e8efe64995108b229b2862cd5752d40d6f" integrity sha512-uph+w6xUOlyV2DLSC6o+fBDzZ5i7+3/TxAsH4h3eC64tlga57oMb96vVlXoMwjR/nN+xyWlsnxtbDkB46M2EPQ== @@ -1370,6 +1399,16 @@ "@typescript-eslint/typescript-estree" "2.31.0" eslint-visitor-keys "^1.1.0" +"@typescript-eslint/parser@^2.33.0": + version "2.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.33.0.tgz#395c0ef229ebef883608f8632a34f0acf02b9bdd" + integrity sha512-AUtmwUUhJoH6yrtxZMHbRUEMsC2G6z5NSxg9KsROOGqNXasM71I8P2NihtumlWTUCRld70vqIZ6Pm4E5PAziEA== + dependencies: + "@types/eslint-visitor-keys" "^1.0.0" + "@typescript-eslint/experimental-utils" "2.33.0" + "@typescript-eslint/typescript-estree" "2.33.0" + eslint-visitor-keys "^1.1.0" + "@typescript-eslint/typescript-estree@2.31.0": version "2.31.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.31.0.tgz#ac536c2d46672aa1f27ba0ec2140d53670635cfd" @@ -1383,6 +1422,19 @@ semver "^6.3.0" tsutils "^3.17.1" +"@typescript-eslint/typescript-estree@2.33.0": + version "2.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.33.0.tgz#33504c050ccafd38f397a645d4e9534d2eccbb5c" + integrity sha512-d8rY6/yUxb0+mEwTShCQF2zYQdLlqihukNfG9IUlLYz5y1CH6G/9XYbrxQLq3Z14RNvkCC6oe+OcFlyUpwUbkg== + dependencies: + debug "^4.1.1" + eslint-visitor-keys "^1.1.0" + glob "^7.1.6" + is-glob "^4.0.1" + lodash "^4.17.15" + semver "^7.3.2" + tsutils "^3.17.1" + "@yarnpkg/lockfile@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" @@ -2485,6 +2537,13 @@ dedent@^0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= +deep-assign@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/deep-assign/-/deep-assign-3.0.0.tgz#c8e4c4d401cba25550a2f0f486a2e75bc5f219a2" + integrity sha512-YX2i9XjJ7h5q/aQ/IM9PEwEnDqETAIYbggmdDB3HLTlSgo1CxPsj6pvhPG68rq6SVE0+p+6Ywsm5fTYNrYtBWw== + dependencies: + is-obj "^1.0.0" + deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" @@ -3919,6 +3978,11 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-obj@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= + is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" @@ -6094,10 +6158,10 @@ react-native-gesture-handler@^1.6.1: invariant "^2.2.4" prop-types "^15.7.2" -react-native-keychain@^4.0.1: - version "4.0.5" - resolved "https://registry.yarnpkg.com/react-native-keychain/-/react-native-keychain-4.0.5.tgz#6d3aef8f2789fc0dfa8149b8400d82ff77be361a" - integrity sha512-TbiO8AO055EPjaxIuKxrCEOB4r81uQCVDFbtcs+e4pEOHxf6z3JgDC0UU0poJElKfCh37b+TFRxQhg6DbsYJBA== +react-native-keychain@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/react-native-keychain/-/react-native-keychain-6.0.0.tgz#687379dd8468e8c37caa3c4d1c91393c6e04cb1d" + integrity sha512-5jt2oS9WNBT/GXBRdb+9KHTQokI1eMRDa50aQFR1koXqUpkULhvxItgQ2CKTd+ZchZQK/hn85qHQOs/gWgUGYA== react-native-mail@standardnotes/react-native-mail#9862c76: version "4.1.0" @@ -6908,11 +6972,15 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" -snjs@standardnotes/snjs#9382050: - version "1.0.7" - resolved "https://codeload.github.com/standardnotes/snjs/tar.gz/93820500617de13df8c0621926d5066dec309be8" +"sncrypto@github:standardnotes/sncrypto#6625bbcc141161eb866475d9786238001f3c514d": + version "1.1.2" + resolved "https://codeload.github.com/standardnotes/sncrypto/tar.gz/6625bbcc141161eb866475d9786238001f3c514d" + +snjs@standardnotes/snjs#a18da1b: + version "1.0.5" + resolved "https://codeload.github.com/standardnotes/snjs/tar.gz/a18da1b197310486781fe215652ded1cfc47e524" dependencies: - lodash "^4.17.15" + sncrypto "github:standardnotes/sncrypto#6625bbcc141161eb866475d9786238001f3c514d" source-map-resolve@^0.5.0: version "0.5.3" @@ -7531,10 +7599,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@^3.8.3: - version "3.8.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" - integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w== +typescript@^3.9.2: + version "3.9.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.2.tgz#64e9c8e9be6ea583c54607677dd4680a1cf35db9" + integrity sha512-q2ktq4n/uLuNNShyayit+DTobV2ApPEo/6so68JaD5ojvc/6GClBipedB9zNWYxRSAlZXAe405Rlijzl6qDiSw== ua-parser-js@^0.7.18: version "0.7.21"