chore: legacy fixes (#2343)
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<I>,
|
||||
): () => void
|
||||
|
||||
getUser(): User | undefined
|
||||
hasAccount(): boolean
|
||||
|
||||
importData(data: BackupFile, awaitSync?: boolean): Promise<ImportDataReturnType>
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
43
packages/web/src/javascripts/Application/DevMode.ts
Normal file
43
packages/web/src/javascripts/Application/DevMode.ts
Normal file
@@ -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}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<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(() => {
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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> })
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user