From d79e7b14b18714de606cffff3456bc3ee9e4cfff Mon Sep 17 00:00:00 2001 From: Mo Date: Mon, 3 Jul 2023 08:03:25 -0500 Subject: [PATCH] chore: legacy fixes (#2343) --- .../src/Domain/Service/Functions.ts | 26 ++++++----- .../Application/ApplicationInterface.ts | 3 ++ .../Domain/Integrity/IntegrityService.spec.ts | 6 +-- packages/services/src/Domain/Sync/SyncMode.ts | 4 +- .../services/src/Domain/Sync/SyncSource.ts | 15 ++++--- .../snjs/lib/Services/Sync/SyncService.ts | 24 ++++++++--- .../snjs/mocha/model_tests/appmodels.test.js | 4 +- .../src/javascripts/Application/DevMode.ts | 43 +++++++++++++++++++ .../javascripts/Application/WebApplication.ts | 33 +++++++------- .../ApplicationView/ApplicationView.tsx | 29 ++++++++++--- .../Components/Preferences/PreferencesMenu.ts | 10 ++--- .../Controllers/LinkingController.spec.ts | 2 +- .../Controllers/LinkingController.tsx | 12 +++--- .../Utils/Dev/PurchaseMockSubscription.ts | 32 -------------- 14 files changed, 148 insertions(+), 95 deletions(-) create mode 100644 packages/web/src/javascripts/Application/DevMode.ts delete mode 100644 packages/web/src/javascripts/Utils/Dev/PurchaseMockSubscription.ts diff --git a/packages/encryption/src/Domain/Service/Functions.ts b/packages/encryption/src/Domain/Service/Functions.ts index a7be0a521..d8daf60c1 100644 --- a/packages/encryption/src/Domain/Service/Functions.ts +++ b/packages/encryption/src/Domain/Service/Functions.ts @@ -9,16 +9,22 @@ export function findDefaultItemsKey(itemsKeys: ItemsKeyInterface[]): ItemsKeyInt return key.isDefault }) - if (defaultKeys.length > 1) { - /** - * Prioritize one that is synced, as neverSynced keys will likely be deleted after - * DownloadFirst sync. - */ - const syncedKeys = defaultKeys.filter((key) => !key.neverSynced) - if (syncedKeys.length > 0) { - return syncedKeys[0] - } + if (defaultKeys.length === 0) { + return undefined } - return defaultKeys[0] + if (defaultKeys.length === 1) { + return defaultKeys[0] + } + + /** + * Prioritize one that is synced, as neverSynced keys will likely be deleted after + * DownloadFirst sync. + */ + const syncedKeys = defaultKeys.filter((key) => !key.neverSynced) + if (syncedKeys.length > 0) { + return syncedKeys[0] + } + + return undefined } diff --git a/packages/services/src/Domain/Application/ApplicationInterface.ts b/packages/services/src/Domain/Application/ApplicationInterface.ts index bcd7c4bc2..35b83e41b 100644 --- a/packages/services/src/Domain/Application/ApplicationInterface.ts +++ b/packages/services/src/Domain/Application/ApplicationInterface.ts @@ -30,6 +30,7 @@ import { DeinitMode } from './DeinitMode' import { DeinitSource } from './DeinitSource' import { UserClientInterface } from '../User/UserClientInterface' import { SessionsClientInterface } from '../Session/SessionsClientInterface' +import { User } from '@standardnotes/responses' export interface ApplicationInterface { deinit(mode: DeinitMode, source: DeinitSource): void @@ -57,6 +58,8 @@ export interface ApplicationInterface { contentType: ContentType | ContentType[], stream: ItemStream, ): () => void + + getUser(): User | undefined hasAccount(): boolean importData(data: BackupFile, awaitSync?: boolean): Promise diff --git a/packages/services/src/Domain/Integrity/IntegrityService.spec.ts b/packages/services/src/Domain/Integrity/IntegrityService.spec.ts index 26c347dbf..00cab5680 100644 --- a/packages/services/src/Domain/Integrity/IntegrityService.spec.ts +++ b/packages/services/src/Domain/Integrity/IntegrityService.spec.ts @@ -63,7 +63,7 @@ describe('IntegrityService', () => { uuid: '1-2-3', }, ], - source: 5, + source: "AfterDownloadFirst", }, type: 'IntegrityCheckCompleted', }, @@ -90,7 +90,7 @@ describe('IntegrityService', () => { { payload: { rawPayloads: [], - source: 5, + source: "AfterDownloadFirst", }, type: 'IntegrityCheckCompleted', }, @@ -140,7 +140,7 @@ describe('IntegrityService', () => { { payload: { rawPayloads: [], - source: 5, + source: "AfterDownloadFirst", }, type: 'IntegrityCheckCompleted', }, diff --git a/packages/services/src/Domain/Sync/SyncMode.ts b/packages/services/src/Domain/Sync/SyncMode.ts index a8a51fbec..7a813c672 100644 --- a/packages/services/src/Domain/Sync/SyncMode.ts +++ b/packages/services/src/Domain/Sync/SyncMode.ts @@ -4,11 +4,11 @@ export enum SyncMode { /** * Performs a standard sync, uploading any dirty items and retrieving items. */ - Default = 1, + Default = 'Default', /** * The first sync for an account, where we first want to download all remote items first * before uploading any dirty items. This allows a consumer, for example, to download * all data to see if user has an items key, and if not, only then create a new one. */ - DownloadFirst = 2, + DownloadFirst = 'DownloadFirst', } diff --git a/packages/services/src/Domain/Sync/SyncSource.ts b/packages/services/src/Domain/Sync/SyncSource.ts index 646157c45..d07433ade 100644 --- a/packages/services/src/Domain/Sync/SyncSource.ts +++ b/packages/services/src/Domain/Sync/SyncSource.ts @@ -1,11 +1,12 @@ /* istanbul ignore file */ export enum SyncSource { - External = 1, - SpawnQueue = 2, - ResolveQueue = 3, - MoreDirtyItems = 4, - AfterDownloadFirst = 5, - IntegrityCheck = 6, - ResolveOutOfSync = 7, + External = 'External', + SpawnQueue = 'SpawnQueue', + ResolveQueue = 'ResolveQueue', + MoreDirtyItems = 'MoreDirtyItems', + DownloadFirst = 'DownloadFirst', + AfterDownloadFirst = 'AfterDownloadFirst', + IntegrityCheck = 'IntegrityCheck', + ResolveOutOfSync = 'ResolveOutOfSync', } diff --git a/packages/snjs/lib/Services/Sync/SyncService.ts b/packages/snjs/lib/Services/Sync/SyncService.ts index 9ecb80911..d6e3319d7 100644 --- a/packages/snjs/lib/Services/Sync/SyncService.ts +++ b/packages/snjs/lib/Services/Sync/SyncService.ts @@ -432,7 +432,15 @@ export class SNSyncService }) await this.payloadManager.emitPayloads(payloads, PayloadEmitSource.LocalChanged) - await this.persistPayloads(payloads) + + /** + * When signing into an 003 account (or an account that is not the latest), the temporary items key will be 004 + * and will not match user account version, triggering a key not found exception. This error resolves once the + * download first sync completes and the correct key is downloaded. We suppress any persistence + * exceptions here to avoid showing an error to the user. + */ + const hidePersistErrorDueToWaitingOnKeyDownload = true + await this.persistPayloads(payloads, { throwError: !hidePersistErrorDueToWaitingOnKeyDownload }) } /** @@ -579,7 +587,8 @@ export class SNSyncService const payloadsNeedingSave = this.popPayloadsNeedingPreSyncSave(decryptedPayloads) - await this.persistPayloads(payloadsNeedingSave) + const hidePersistErrorDueToWaitingOnKeyDownload = options.mode === SyncMode.DownloadFirst + await this.persistPayloads(payloadsNeedingSave, { throwError: !hidePersistErrorDueToWaitingOnKeyDownload }) if (options.onPresyncSave) { options.onPresyncSave() @@ -1336,14 +1345,19 @@ export class SNSyncService await this.persistPayloads(payloads) } - public async persistPayloads(payloads: FullyFormedPayloadInterface[]) { + public async persistPayloads( + payloads: FullyFormedPayloadInterface[], + options: { throwError: boolean } = { throwError: true }, + ) { if (payloads.length === 0 || this.dealloced) { return } return this.storageService.savePayloads(payloads).catch((error) => { - void this.notifyEvent(SyncEvent.DatabaseWriteError, error) - SNLog.error(error) + if (options.throwError) { + void this.notifyEvent(SyncEvent.DatabaseWriteError, error) + SNLog.error(error) + } }) } diff --git a/packages/snjs/mocha/model_tests/appmodels.test.js b/packages/snjs/mocha/model_tests/appmodels.test.js index f04ddc2f4..ce9688fca 100644 --- a/packages/snjs/mocha/model_tests/appmodels.test.js +++ b/packages/snjs/mocha/model_tests/appmodels.test.js @@ -50,7 +50,9 @@ describe('app models', () => { const epoch = new Date(0) expect(item.serverUpdatedAt - epoch).to.equal(0) expect(item.created_at - epoch).to.be.above(0) - expect(new Date() - item.created_at).to.be.below(5) // < 5ms + + const presentThresholdMs = 10 + expect(new Date() - item.created_at).to.be.below(presentThresholdMs) }) it('handles delayed mapping', async function () { diff --git a/packages/web/src/javascripts/Application/DevMode.ts b/packages/web/src/javascripts/Application/DevMode.ts new file mode 100644 index 000000000..8b1e2f0a6 --- /dev/null +++ b/packages/web/src/javascripts/Application/DevMode.ts @@ -0,0 +1,43 @@ +import { InternalFeature, InternalFeatureService } from '@standardnotes/snjs' +import { WebApplicationInterface } from '@standardnotes/ui-services' + +export class DevMode { + constructor(private application: WebApplicationInterface) { + InternalFeatureService.get().enableFeature(InternalFeature.Vaults) + } + + /** Valid only when running a mock event publisher on port 3124 */ + async purchaseMockSubscription() { + const subscriptionId = 2000 + const email = this.application.getUser()?.email + const response = await fetch('http://localhost:3124/events', { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + eventType: 'SUBSCRIPTION_PURCHASED', + eventPayload: { + userEmail: email, + subscriptionId: subscriptionId, + subscriptionName: 'PRO_PLAN', + subscriptionExpiresAt: (new Date().getTime() + 3_600_000) * 1_000, + timestamp: Date.now(), + offline: false, + discountCode: null, + limitedDiscountPurchased: false, + newSubscriber: true, + totalActiveSubscriptionsCount: 1, + userRegisteredAt: 1, + billingFrequency: 12, + payAmount: 59.0, + }, + }), + }) + + if (!response.ok) { + console.error(`Failed to publish mocked event: ${response.status} ${response.statusText}`) + } + } +} diff --git a/packages/web/src/javascripts/Application/WebApplication.ts b/packages/web/src/javascripts/Application/WebApplication.ts index 6c9548bf8..2a9d7e5e4 100644 --- a/packages/web/src/javascripts/Application/WebApplication.ts +++ b/packages/web/src/javascripts/Application/WebApplication.ts @@ -51,18 +51,21 @@ import { FeatureName } from '@/Controllers/FeatureName' import { ItemGroupController } from '@/Components/NoteView/Controller/ItemGroupController' import { VisibilityObserver } from './VisibilityObserver' import { MomentsService } from '@/Controllers/Moments/MomentsService' -import { purchaseMockSubscription } from '@/Utils/Dev/PurchaseMockSubscription' +import { DevMode } from './DevMode' export type WebEventObserver = (event: WebAppEvent, data?: unknown) => void export class WebApplication extends SNApplication implements WebApplicationInterface { - private webServices!: WebServices - private webEventObservers: WebEventObserver[] = [] - public itemControllerGroup: ItemGroupController - private mobileWebReceiver?: MobileWebReceiver - private androidBackHandler?: AndroidBackHandler + public readonly itemControllerGroup: ItemGroupController public readonly routeService: RouteServiceInterface - private visibilityObserver?: VisibilityObserver + + private readonly webServices!: WebServices + private readonly webEventObservers: WebEventObserver[] = [] + private readonly mobileWebReceiver?: MobileWebReceiver + private readonly androidBackHandler?: AndroidBackHandler + private readonly visibilityObserver?: VisibilityObserver + + public readonly devMode?: DevMode constructor( deviceInterface: WebOrDesktopDevice, @@ -91,6 +94,10 @@ export class WebApplication extends SNApplication implements WebApplicationInter u2fAuthenticatorVerificationPromptFunction: startAuthentication, }) + if (isDev) { + this.devMode = new DevMode(this) + } + makeObservable(this, { dealloced: observable, }) @@ -152,7 +159,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter ;(service as { application?: WebApplication }).application = undefined } - this.webServices = {} as WebServices + ;(this.webServices as unknown) = undefined this.itemControllerGroup.deinit() ;(this.itemControllerGroup as unknown) = undefined @@ -165,7 +172,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter if (this.visibilityObserver) { this.visibilityObserver.deinit() - this.visibilityObserver = undefined + ;(this.visibilityObserver as unknown) = undefined } } catch (error) { console.error('Error while deiniting application', error) @@ -458,12 +465,4 @@ export class WebApplication extends SNApplication implements WebApplicationInter generateUUID(): string { return this.options.crypto.generateUUID() } - - dev__purchaseMockSubscription() { - if (!isDev) { - throw new Error('This method is only available in dev mode') - } - - void purchaseMockSubscription(this.getUser()?.email as string, 2000) - } } diff --git a/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx b/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx index f2f806505..bc9c0ec07 100644 --- a/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx +++ b/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx @@ -9,7 +9,7 @@ import PreferencesViewWrapper from '@/Components/Preferences/PreferencesViewWrap import ChallengeModal from '@/Components/ChallengeModal/ChallengeModal' import NotesContextMenu from '@/Components/NotesContextMenu/NotesContextMenu' import PurchaseFlowWrapper from '@/Components/PurchaseFlow/PurchaseFlowWrapper' -import { FunctionComponent, useCallback, useEffect, useMemo, useState, lazy } from 'react' +import { FunctionComponent, useCallback, useEffect, useMemo, useState, lazy, useRef } from 'react' import RevisionHistoryModal from '@/Components/RevisionHistoryModal/RevisionHistoryModal' import PremiumModalProvider from '@/Hooks/usePremiumModal' import ConfirmSignoutContainer from '@/Components/ConfirmSignoutModal/ConfirmSignoutModal' @@ -44,6 +44,9 @@ const ApplicationView: FunctionComponent = ({ application, mainApplicatio const [needsUnlock, setNeedsUnlock] = useState(true) const [challenges, setChallenges] = useState([]) + const currentWriteErrorDialog = useRef | null>(null) + const currentLoadErrorDialog = useRef | null>(null) + const viewControllerManager = application.getViewControllerManager() useEffect(() => { @@ -120,13 +123,25 @@ const ApplicationView: FunctionComponent = ({ application, mainApplicatio } else if (eventName === ApplicationEvent.Launched) { onAppLaunch() } else if (eventName === ApplicationEvent.LocalDatabaseReadError) { - alertDialog({ - text: 'Unable to load local database. Please restart the app and try again.', - }).catch(console.error) + if (!currentLoadErrorDialog.current) { + alertDialog({ + text: 'Unable to load local database. Please restart the app and try again.', + }) + .then(() => { + currentLoadErrorDialog.current = null + }) + .catch(console.error) + } } else if (eventName === ApplicationEvent.LocalDatabaseWriteError) { - alertDialog({ - text: 'Unable to write to local database. Please restart the app and try again.', - }).catch(console.error) + if (!currentWriteErrorDialog.current) { + currentWriteErrorDialog.current = alertDialog({ + text: 'Unable to write to local database. Please restart the app and try again.', + }) + .then(() => { + currentWriteErrorDialog.current = null + }) + .catch(console.error) + } } else if (eventName === ApplicationEvent.BiometricsSoftLockEngaged) { setNeedsUnlock(true) } else if (eventName === ApplicationEvent.BiometricsSoftLockDisengaged) { diff --git a/packages/web/src/javascripts/Components/Preferences/PreferencesMenu.ts b/packages/web/src/javascripts/Components/Preferences/PreferencesMenu.ts index 4e121a4a6..67ed73c11 100644 --- a/packages/web/src/javascripts/Components/Preferences/PreferencesMenu.ts +++ b/packages/web/src/javascripts/Components/Preferences/PreferencesMenu.ts @@ -45,17 +45,17 @@ const READY_PREFERENCES_MENU_ITEMS: PreferencesMenuItem[] = [ { id: 'help-feedback', label: 'Help & feedback', icon: 'help' }, ] -if (featureTrunkVaultsEnabled()) { - PREFERENCES_MENU_ITEMS.splice(3, 0, { id: 'vaults', label: 'Vaults', icon: 'safe-square' }) - READY_PREFERENCES_MENU_ITEMS.splice(3, 0, { id: 'vaults', label: 'Vaults', icon: 'safe-square' }) -} - export class PreferencesMenu { private _selectedPane: PreferenceId = 'account' private _menu: PreferencesMenuItem[] private _extensionLatestVersions: PackageProvider = new PackageProvider(new Map()) constructor(private application: WebApplication, private readonly _enableUnfinishedFeatures: boolean) { + if (featureTrunkVaultsEnabled()) { + PREFERENCES_MENU_ITEMS.splice(3, 0, { id: 'vaults', label: 'Vaults', icon: 'safe-square' }) + READY_PREFERENCES_MENU_ITEMS.splice(3, 0, { id: 'vaults', label: 'Vaults', icon: 'safe-square' }) + } + this._menu = this._enableUnfinishedFeatures ? PREFERENCES_MENU_ITEMS : READY_PREFERENCES_MENU_ITEMS this.loadLatestVersions() diff --git a/packages/web/src/javascripts/Controllers/LinkingController.spec.ts b/packages/web/src/javascripts/Controllers/LinkingController.spec.ts index 9fa05f0b9..f8c5fbb2e 100644 --- a/packages/web/src/javascripts/Controllers/LinkingController.spec.ts +++ b/packages/web/src/javascripts/Controllers/LinkingController.spec.ts @@ -62,12 +62,12 @@ describe('LinkingController', () => { alerts: {} as jest.Mocked, sync: {} as jest.Mocked, mutator: {} as jest.Mocked, + itemControllerGroup: {} as jest.Mocked, } as unknown as jest.Mocked application.getPreference = jest.fn() application.addSingleEventObserver = jest.fn() application.streamItems = jest.fn() - application.itemControllerGroup = {} as jest.Mocked application.sync.sync = jest.fn() Object.defineProperty(application, 'items', { value: {} as jest.Mocked }) diff --git a/packages/web/src/javascripts/Controllers/LinkingController.tsx b/packages/web/src/javascripts/Controllers/LinkingController.tsx index 6afeef1b6..4fabe89b0 100644 --- a/packages/web/src/javascripts/Controllers/LinkingController.tsx +++ b/packages/web/src/javascripts/Controllers/LinkingController.tsx @@ -197,11 +197,13 @@ export class LinkingController extends AbstractViewController { const linkNoteAndFile = async (note: SNNote, file: FileItem) => { const updatedFile = await this.application.mutator.associateFileWithNote(file, note) - if (updatedFile && featureTrunkVaultsEnabled()) { - const noteVault = this.application.vaults.getItemVault(note) - const fileVault = this.application.vaults.getItemVault(updatedFile) - if (noteVault && !fileVault) { - await this.application.vaults.moveItemToVault(noteVault, file) + if (featureTrunkVaultsEnabled()) { + if (updatedFile) { + const noteVault = this.application.vaults.getItemVault(note) + const fileVault = this.application.vaults.getItemVault(updatedFile) + if (noteVault && !fileVault) { + await this.application.vaults.moveItemToVault(noteVault, file) + } } } } diff --git a/packages/web/src/javascripts/Utils/Dev/PurchaseMockSubscription.ts b/packages/web/src/javascripts/Utils/Dev/PurchaseMockSubscription.ts deleted file mode 100644 index a1ce59efb..000000000 --- a/packages/web/src/javascripts/Utils/Dev/PurchaseMockSubscription.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** Valid only when running a mock event publisher on port 3124 */ -export async function purchaseMockSubscription(email: string, subscriptionId: number) { - const response = await fetch('http://localhost:3124/events', { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - eventType: 'SUBSCRIPTION_PURCHASED', - eventPayload: { - userEmail: email, - subscriptionId: subscriptionId, - subscriptionName: 'PRO_PLAN', - subscriptionExpiresAt: (new Date().getTime() + 3_600_000) * 1_000, - timestamp: Date.now(), - offline: false, - discountCode: null, - limitedDiscountPurchased: false, - newSubscriber: true, - totalActiveSubscriptionsCount: 1, - userRegisteredAt: 1, - billingFrequency: 12, - payAmount: 59.0, - }, - }), - }) - - if (!response.ok) { - console.error(`Failed to publish mocked event: ${response.status} ${response.statusText}`) - } -}