Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: legacy fixes #2343

Merged
merged 5 commits into from Jul 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 16 additions & 10 deletions packages/encryption/src/Domain/Service/Functions.ts
Expand Up @@ -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
}
Expand Up @@ -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
Expand Down Expand Up @@ -57,6 +58,8 @@ export interface ApplicationInterface {
contentType: ContentType | ContentType[],
stream: ItemStream<I>,
): () => void

getUser(): User | undefined
hasAccount(): boolean

importData(data: BackupFile, awaitSync?: boolean): Promise<ImportDataReturnType>
Expand Down
Expand Up @@ -63,7 +63,7 @@ describe('IntegrityService', () => {
uuid: '1-2-3',
},
],
source: 5,
source: "AfterDownloadFirst",
},
type: 'IntegrityCheckCompleted',
},
Expand All @@ -90,7 +90,7 @@ describe('IntegrityService', () => {
{
payload: {
rawPayloads: [],
source: 5,
source: "AfterDownloadFirst",
},
type: 'IntegrityCheckCompleted',
},
Expand Down Expand Up @@ -140,7 +140,7 @@ describe('IntegrityService', () => {
{
payload: {
rawPayloads: [],
source: 5,
source: "AfterDownloadFirst",
},
type: 'IntegrityCheckCompleted',
},
Expand Down
4 changes: 2 additions & 2 deletions packages/services/src/Domain/Sync/SyncMode.ts
Expand Up @@ -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',
}
15 changes: 8 additions & 7 deletions 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',
}
24 changes: 19 additions & 5 deletions packages/snjs/lib/Services/Sync/SyncService.ts
Expand Up @@ -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 })
}

/**
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
}
})
}

Expand Down
4 changes: 3 additions & 1 deletion packages/snjs/mocha/model_tests/appmodels.test.js
Expand Up @@ -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 () {
Expand Down
43 changes: 43 additions & 0 deletions 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}`)
}
}
}
33 changes: 16 additions & 17 deletions packages/web/src/javascripts/Application/WebApplication.ts
Expand Up @@ -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,
Expand Down Expand Up @@ -91,6 +94,10 @@ export class WebApplication extends SNApplication implements WebApplicationInter
u2fAuthenticatorVerificationPromptFunction: startAuthentication,
})

if (isDev) {
this.devMode = new DevMode(this)
}

makeObservable(this, {
dealloced: observable,
})
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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)
}
}
Expand Up @@ -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'
Expand Down Expand Up @@ -44,6 +44,9 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
const [needsUnlock, setNeedsUnlock] = useState(true)
const [challenges, setChallenges] = useState<Challenge[]>([])

const currentWriteErrorDialog = useRef<Promise<void> | null>(null)
const currentLoadErrorDialog = useRef<Promise<void> | null>(null)

const viewControllerManager = application.getViewControllerManager()

useEffect(() => {
Expand Down Expand Up @@ -120,13 +123,25 @@ const ApplicationView: FunctionComponent<Props> = ({ 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) {
Expand Down
Expand Up @@ -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()
Expand Down
Expand Up @@ -62,12 +62,12 @@ describe('LinkingController', () => {
alerts: {} as jest.Mocked<WebApplication['alerts']>,
sync: {} as jest.Mocked<WebApplication['sync']>,
mutator: {} as jest.Mocked<WebApplication['mutator']>,
itemControllerGroup: {} as jest.Mocked<WebApplication['itemControllerGroup']>,
} as unknown as jest.Mocked<WebApplication>

application.getPreference = jest.fn()
application.addSingleEventObserver = jest.fn()
application.streamItems = jest.fn()
application.itemControllerGroup = {} as jest.Mocked<WebApplication['itemControllerGroup']>
application.sync.sync = jest.fn()

Object.defineProperty(application, 'items', { value: {} as jest.Mocked<ItemManagerInterface> })
Expand Down
12 changes: 7 additions & 5 deletions packages/web/src/javascripts/Controllers/LinkingController.tsx
Expand Up @@ -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)
}
}
}
}
Expand Down