fix: Fixes issue where lock screen would not use previously active theme (#2372)

This commit is contained in:
Mo
2023-07-26 15:50:08 -05:00
committed by GitHub
parent 86fc4c684d
commit d268c02ab3
88 changed files with 1118 additions and 716 deletions

View File

@@ -2,8 +2,8 @@ import { ItemInterface, SNFeatureRepo } from '@standardnotes/models'
import { SyncService } from '../Sync/SyncService'
import { SettingName } from '@standardnotes/settings'
import { FeaturesService } from '@Lib/Services/Features'
import { RoleName, ContentType } from '@standardnotes/domain-core'
import { FeatureIdentifier, GetFeatures } from '@standardnotes/features'
import { RoleName, ContentType, Uuid } from '@standardnotes/domain-core'
import { NativeFeatureIdentifier, GetFeatures } from '@standardnotes/features'
import { WebSocketsService } from '../Api/WebsocketsService'
import { SettingsService } from '../Settings'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
@@ -44,6 +44,7 @@ describe('FeaturesService', () => {
let roles: string[]
let items: ItemInterface[]
let internalEventBus: InternalEventBusInterface
let featureService: FeaturesService
const createService = () => {
return new FeaturesService(
@@ -118,23 +119,81 @@ describe('FeaturesService', () => {
internalEventBus = {} as jest.Mocked<InternalEventBusInterface>
internalEventBus.publish = jest.fn()
internalEventBus.addEventHandler = jest.fn()
featureService = new FeaturesService(
storageService,
itemManager,
mutator,
subscriptions,
apiService,
webSocketsService,
settingsService,
userService,
syncService,
alertService,
sessionManager,
crypto,
internalEventBus,
)
})
describe('experimental features', () => {
it('enables/disables an experimental feature', async () => {
storageService.getValue = jest.fn().mockReturnValue(GetFeatures())
const featuresService = createService()
featuresService.getExperimentalFeatures = jest.fn().mockReturnValue([FeatureIdentifier.PlusEditor])
featuresService.initializeFromDisk()
featureService.getExperimentalFeatures = jest.fn().mockReturnValue([NativeFeatureIdentifier.TYPES.PlusEditor])
featureService.initializeFromDisk()
featuresService.enableExperimentalFeature(FeatureIdentifier.PlusEditor)
featureService.enableExperimentalFeature(NativeFeatureIdentifier.TYPES.PlusEditor)
expect(featuresService.isExperimentalFeatureEnabled(FeatureIdentifier.PlusEditor)).toEqual(true)
expect(featureService.isExperimentalFeatureEnabled(NativeFeatureIdentifier.TYPES.PlusEditor)).toEqual(true)
featuresService.disableExperimentalFeature(FeatureIdentifier.PlusEditor)
featureService.disableExperimentalFeature(NativeFeatureIdentifier.TYPES.PlusEditor)
expect(featuresService.isExperimentalFeatureEnabled(FeatureIdentifier.PlusEditor)).toEqual(false)
expect(featureService.isExperimentalFeatureEnabled(NativeFeatureIdentifier.TYPES.PlusEditor)).toEqual(false)
})
})
describe('hasFirstPartyOnlineSubscription', () => {
it('should be true if signed into first party server and has online subscription', () => {
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true)
expect(featureService.hasFirstPartyOnlineSubscription()).toEqual(true)
})
it('should not be true if not signed into first party server', () => {
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false)
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true)
expect(featureService.hasFirstPartyOnlineSubscription()).toEqual(false)
})
it('should not be true if no online subscription', () => {
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(false)
expect(featureService.hasFirstPartyOnlineSubscription()).toEqual(false)
})
})
describe('hasPaidAnyPartyOnlineOrOfflineSubscription', () => {
it('should return true if onlineRolesIncludePaidSubscription', () => {
featureService.onlineRolesIncludePaidSubscription = jest.fn().mockReturnValue(true)
expect(featureService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true)
})
it('should return true if hasOfflineRepo', () => {
featureService.hasOfflineRepo = jest.fn().mockReturnValue(true)
expect(featureService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true)
})
it('should return true if hasFirstPartyOnlineSubscription', () => {
featureService.hasFirstPartyOnlineSubscription = jest.fn().mockReturnValue(true)
expect(featureService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true)
})
})
@@ -148,40 +207,40 @@ describe('FeaturesService', () => {
describe('updateRoles()', () => {
it('setRoles should notify event if roles changed', async () => {
storageService.getValue = jest.fn().mockReturnValue(roles)
const featuresService = createService()
featuresService.initializeFromDisk()
const mock = (featuresService['notifyEvent'] = jest.fn())
featureService.initializeFromDisk()
const mock = (featureService['notifyEvent'] = jest.fn())
const newRoles = [...roles, RoleName.NAMES.PlusUser]
featuresService.setOnlineRoles(newRoles)
featureService.setOnlineRoles(newRoles)
expect(mock.mock.calls[0][0]).toEqual(FeaturesEvent.UserRolesChanged)
})
it('should notify of subscription purchase', async () => {
storageService.getValue = jest.fn().mockReturnValue(roles)
const featuresService = createService()
featuresService.initializeFromDisk()
const spy = jest.spyOn(featuresService, 'notifyEvent' as never)
featureService.initializeFromDisk()
const spy = jest.spyOn(featureService, 'notifyEvent' as never)
const newRoles = [...roles, RoleName.NAMES.ProUser]
await featuresService.updateOnlineRolesWithNewValues(newRoles)
await featureService.updateOnlineRolesWithNewValues(newRoles)
expect(spy.mock.calls[1][0]).toEqual(FeaturesEvent.DidPurchaseSubscription)
})
it('should not notify of subscription purchase on initial roles load after sign in', async () => {
storageService.getValue = jest.fn().mockReturnValue(roles)
const featuresService = createService()
featuresService.initializeFromDisk()
featuresService['onlineRoles'] = []
const spy = jest.spyOn(featuresService, 'notifyEvent' as never)
featureService.initializeFromDisk()
featureService['onlineRoles'] = []
const spy = jest.spyOn(featureService, 'notifyEvent' as never)
const newRoles = [...roles, RoleName.NAMES.ProUser]
await featuresService.updateOnlineRolesWithNewValues(newRoles)
await featureService.updateOnlineRolesWithNewValues(newRoles)
const triggeredEvents = spy.mock.calls.map((call) => call[0])
expect(triggeredEvents).not.toContain(FeaturesEvent.DidPurchaseSubscription)
@@ -189,11 +248,11 @@ describe('FeaturesService', () => {
it('saves new roles to storage if a role has been added', async () => {
storageService.getValue = jest.fn().mockReturnValue(roles)
const featuresService = createService()
featuresService.initializeFromDisk()
featureService.initializeFromDisk()
const newRoles = [...roles, RoleName.NAMES.ProUser]
await featuresService.updateOnlineRolesWithNewValues(newRoles)
await featureService.updateOnlineRolesWithNewValues(newRoles)
expect(storageService.setValue).toHaveBeenCalledWith(StorageKey.UserRoles, newRoles)
})
@@ -201,134 +260,162 @@ describe('FeaturesService', () => {
const newRoles = [RoleName.NAMES.CoreUser]
storageService.getValue = jest.fn().mockReturnValue(roles)
const featuresService = createService()
featuresService.initializeFromDisk()
await featuresService.updateOnlineRolesWithNewValues(newRoles)
featureService.initializeFromDisk()
await featureService.updateOnlineRolesWithNewValues(newRoles)
expect(storageService.setValue).toHaveBeenCalledWith(StorageKey.UserRoles, newRoles)
})
it('role-based feature status', async () => {
const featuresService = createService()
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true)
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled)
expect(featuresService.getFeatureStatus(FeatureIdentifier.SuperEditor)).toBe(FeatureStatus.Entitled)
expect(
featureService.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.MidnightTheme).getValue(),
),
).toBe(FeatureStatus.Entitled)
expect(
featureService.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SuperEditor).getValue(),
),
).toBe(FeatureStatus.Entitled)
})
it('feature status with no paid role', async () => {
const featuresService = createService()
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(false)
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.NoUserSubscription)
expect(featuresService.getFeatureStatus(FeatureIdentifier.PlusEditor)).toBe(FeatureStatus.NoUserSubscription)
expect(featuresService.getFeatureStatus(FeatureIdentifier.SheetsEditor)).toBe(FeatureStatus.NoUserSubscription)
expect(
featureService.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.MidnightTheme).getValue(),
),
).toBe(FeatureStatus.NoUserSubscription)
expect(
featureService.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.PlusEditor).getValue(),
),
).toBe(FeatureStatus.NoUserSubscription)
expect(
featureService.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SheetsEditor).getValue(),
),
).toBe(FeatureStatus.NoUserSubscription)
})
it('role-based features while not signed into first party server', async () => {
const featuresService = createService()
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false)
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.ProUser])
await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.ProUser])
expect(featuresService.getFeatureStatus(FeatureIdentifier.SuperEditor)).toBe(FeatureStatus.NoUserSubscription)
expect(
featureService.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SuperEditor).getValue(),
),
).toBe(FeatureStatus.NoUserSubscription)
})
it('third party feature status', async () => {
const featuresService = createService()
itemManager.getDisplayableComponents = jest
.fn()
.mockReturnValue([{ identifier: 'third-party-theme' }, { identifier: 'third-party-editor', isExpired: true }])
.mockReturnValue([
{ uuid: '00000000-0000-0000-0000-000000000001' },
{ uuid: '00000000-0000-0000-0000-000000000002', isExpired: true },
])
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
expect(featuresService.getFeatureStatus('third-party-theme' as FeatureIdentifier)).toBe(FeatureStatus.Entitled)
expect(featuresService.getFeatureStatus('third-party-editor' as FeatureIdentifier)).toBe(
expect(featureService.getFeatureStatus(Uuid.create('00000000-0000-0000-0000-000000000001').getValue())).toBe(
FeatureStatus.Entitled,
)
expect(featureService.getFeatureStatus(Uuid.create('00000000-0000-0000-0000-000000000002').getValue())).toBe(
FeatureStatus.InCurrentPlanButExpired,
)
expect(featuresService.getFeatureStatus('missing-feature-identifier' as FeatureIdentifier)).toBe(
expect(featureService.getFeatureStatus(Uuid.create('00000000-0000-0000-0000-000000000003').getValue())).toBe(
FeatureStatus.NoUserSubscription,
)
})
it('feature status should be not entitled if no account or offline repo', async () => {
const featuresService = createService()
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false)
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.NoUserSubscription)
expect(featuresService.getFeatureStatus(FeatureIdentifier.TokenVaultEditor)).toBe(
FeatureStatus.NoUserSubscription,
)
expect(
featureService.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.MidnightTheme).getValue(),
),
).toBe(FeatureStatus.NoUserSubscription)
expect(
featureService.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.TokenVaultEditor).getValue(),
),
).toBe(FeatureStatus.NoUserSubscription)
})
it('feature status for offline subscription', async () => {
const featuresService = createService()
featureService.hasFirstPartyOfflineSubscription = jest.fn().mockReturnValue(true)
featureService.setOfflineRoles([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
featuresService.hasFirstPartyOfflineSubscription = jest.fn().mockReturnValue(true)
featuresService.setOfflineRoles([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled)
expect(featuresService.getFeatureStatus(FeatureIdentifier.TokenVaultEditor)).toBe(FeatureStatus.Entitled)
expect(
featureService.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.MidnightTheme).getValue(),
),
).toBe(FeatureStatus.Entitled)
expect(
featureService.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.TokenVaultEditor).getValue(),
),
).toBe(FeatureStatus.Entitled)
})
it('feature status for deprecated feature and no subscription', async () => {
const featuresService = createService()
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(false)
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
expect(featuresService.getFeatureStatus(FeatureIdentifier.DeprecatedFileSafe as FeatureIdentifier)).toBe(
FeatureStatus.NoUserSubscription,
)
expect(
featureService.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.DeprecatedFileSafe).getValue(),
),
).toBe(FeatureStatus.NoUserSubscription)
})
it('feature status for deprecated feature with subscription', async () => {
const featuresService = createService()
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true)
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
expect(featuresService.getFeatureStatus(FeatureIdentifier.DeprecatedFileSafe as FeatureIdentifier)).toBe(
FeatureStatus.Entitled,
)
expect(
featureService.getFeatureStatus(
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.DeprecatedFileSafe).getValue(),
),
).toBe(FeatureStatus.Entitled)
})
it('has paid subscription', async () => {
const featuresService = createService()
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true)
expect(featuresService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toBeFalsy
expect(featureService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toBeFalsy
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
expect(featuresService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true)
expect(featureService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true)
})
it('has paid subscription should be true if offline repo and signed into third party server', async () => {
const featuresService = createService()
await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
featuresService.hasOfflineRepo = jest.fn().mockReturnValue(true)
featureService.hasOfflineRepo = jest.fn().mockReturnValue(true)
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false)
expect(featuresService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true)
expect(featureService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true)
})
})
@@ -343,8 +430,7 @@ describe('FeaturesService', () => {
},
} as never)
const featuresService = createService()
await featuresService.migrateFeatureRepoToUserSetting([extensionRepoItem])
await featureService.migrateFeatureRepoToUserSetting([extensionRepoItem])
expect(settingsService.updateSetting).toHaveBeenCalledWith(
SettingName.create(SettingName.NAMES.ExtensionKey).getValue(),
extensionKey,
@@ -369,8 +455,7 @@ describe('FeaturesService', () => {
const installUrl = 'http://example.com'
crypto.base64Decode = jest.fn().mockReturnValue(installUrl)
const featuresService = createService()
const result = await featuresService.downloadRemoteThirdPartyFeature(installUrl)
const result = await featureService.downloadRemoteThirdPartyFeature(installUrl)
expect(result).toBeUndefined()
})
@@ -389,17 +474,14 @@ describe('FeaturesService', () => {
const installUrl = 'http://example.com'
crypto.base64Decode = jest.fn().mockReturnValue(installUrl)
const featuresService = createService()
const result = await featuresService.downloadRemoteThirdPartyFeature(installUrl)
const result = await featureService.downloadRemoteThirdPartyFeature(installUrl)
expect(result).toBeUndefined()
})
})
describe('sortRolesByHierarchy', () => {
it('should sort given roles according to role hierarchy', () => {
const featuresService = createService()
const sortedRoles = featuresService.rolesBySorting([
const sortedRoles = featureService.rolesBySorting([
RoleName.NAMES.ProUser,
RoleName.NAMES.CoreUser,
RoleName.NAMES.PlusUser,
@@ -411,50 +493,42 @@ describe('FeaturesService', () => {
describe('hasMinimumRole', () => {
it('should be false if core user checks for plus role', async () => {
const featuresService = createService()
await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
const hasPlusUserRole = featuresService.hasMinimumRole(RoleName.NAMES.PlusUser)
const hasPlusUserRole = featureService.hasMinimumRole(RoleName.NAMES.PlusUser)
expect(hasPlusUserRole).toBe(false)
})
it('should be false if plus user checks for pro role', async () => {
const featuresService = createService()
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true)
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.PlusUser, RoleName.NAMES.CoreUser])
await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.PlusUser, RoleName.NAMES.CoreUser])
const hasProUserRole = featuresService.hasMinimumRole(RoleName.NAMES.ProUser)
const hasProUserRole = featureService.hasMinimumRole(RoleName.NAMES.ProUser)
expect(hasProUserRole).toBe(false)
})
it('should be true if pro user checks for core user', async () => {
const featuresService = createService()
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true)
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser])
await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser])
const hasCoreUserRole = featuresService.hasMinimumRole(RoleName.NAMES.CoreUser)
const hasCoreUserRole = featureService.hasMinimumRole(RoleName.NAMES.CoreUser)
expect(hasCoreUserRole).toBe(true)
})
it('should be true if pro user checks for pro user', async () => {
const featuresService = createService()
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true)
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser])
await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser])
const hasProUserRole = featuresService.hasMinimumRole(RoleName.NAMES.ProUser)
const hasProUserRole = featureService.hasMinimumRole(RoleName.NAMES.ProUser)
expect(hasProUserRole).toBe(true)
})

View File

@@ -1,14 +1,14 @@
import { MigrateFeatureRepoToUserSettingUseCase } from './UseCase/MigrateFeatureRepoToUserSetting'
import { arraysEqual, removeFromArray, lastElement } from '@standardnotes/utils'
import { ClientDisplayableError } from '@standardnotes/responses'
import { RoleName, ContentType } from '@standardnotes/domain-core'
import { RoleName, ContentType, Uuid } from '@standardnotes/domain-core'
import { PROD_OFFLINE_FEATURES_URL } from '../../Hosts'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { WebSocketsService } from '../Api/WebsocketsService'
import { WebSocketsServiceEvent } from '../Api/WebSocketsServiceEvent'
import { TRUSTED_CUSTOM_EXTENSIONS_HOSTS, TRUSTED_FEATURE_HOSTS } from '@Lib/Hosts'
import { UserRolesChangedEvent } from '@standardnotes/domain-events'
import { ExperimentalFeatures, FindNativeFeature, FeatureIdentifier } from '@standardnotes/features'
import { ExperimentalFeatures, FindNativeFeature, NativeFeatureIdentifier } from '@standardnotes/features'
import {
SNFeatureRepo,
FeatureRepoContent,
@@ -64,7 +64,7 @@ export class FeaturesService
{
private onlineRoles: string[] = []
private offlineRoles: string[] = []
private enabledExperimentalFeatures: FeatureIdentifier[] = []
private enabledExperimentalFeatures: string[] = []
private getFeatureStatusUseCase = new GetFeatureStatusUseCase(this.items)
@@ -136,40 +136,47 @@ export class FeaturesService
)
}
public initializeFromDisk(): void {
initializeFromDisk(): void {
this.onlineRoles = this.storage.getValue<string[]>(StorageKey.UserRoles, undefined, [])
this.offlineRoles = this.storage.getValue<string[]>(StorageKey.OfflineUserRoles, undefined, [])
this.enabledExperimentalFeatures = this.storage.getValue(StorageKey.ExperimentalFeatures, undefined, [])
}
async handleEvent(event: InternalEventInterface): Promise<void> {
if (event.type === ApiServiceEvent.MetaReceived) {
if (!this.sync) {
this.log('Handling events interrupted. Sync service is not yet initialized.', event)
return
switch (event.type) {
case ApiServiceEvent.MetaReceived: {
if (!this.sync) {
this.log('Handling events interrupted. Sync service is not yet initialized.', event)
return
}
const { userRoles } = event.payload as MetaReceivedData
void this.updateOnlineRolesWithNewValues(userRoles.map((role) => role.name))
break
}
const { userRoles } = event.payload as MetaReceivedData
void this.updateOnlineRolesWithNewValues(userRoles.map((role) => role.name))
}
if (event.type === ApplicationEvent.ApplicationStageChanged) {
const stage = (event.payload as ApplicationStageChangedEventPayload).stage
if (stage === ApplicationStage.FullSyncCompleted_13) {
if (!this.hasFirstPartyOnlineSubscription()) {
const offlineRepo = this.getOfflineRepo()
if (offlineRepo) {
void this.downloadOfflineRoles(offlineRepo)
case ApplicationEvent.ApplicationStageChanged: {
const stage = (event.payload as ApplicationStageChangedEventPayload).stage
switch (stage) {
case ApplicationStage.StorageDecrypted_09: {
this.initializeFromDisk()
break
}
case ApplicationStage.FullSyncCompleted_13: {
if (!this.hasFirstPartyOnlineSubscription()) {
const offlineRepo = this.getOfflineRepo()
if (offlineRepo) {
void this.downloadOfflineRoles(offlineRepo)
}
}
break
}
}
}
}
}
public enableExperimentalFeature(identifier: FeatureIdentifier): void {
public enableExperimentalFeature(identifier: string): void {
this.enabledExperimentalFeatures.push(identifier)
void this.storage.setValue(StorageKey.ExperimentalFeatures, this.enabledExperimentalFeatures)
@@ -177,7 +184,7 @@ export class FeaturesService
void this.notifyEvent(FeaturesEvent.FeaturesAvailabilityChanged)
}
public disableExperimentalFeature(identifier: FeatureIdentifier): void {
public disableExperimentalFeature(identifier: string): void {
removeFromArray(this.enabledExperimentalFeatures, identifier)
void this.storage.setValue(StorageKey.ExperimentalFeatures, this.enabledExperimentalFeatures)
@@ -195,7 +202,7 @@ export class FeaturesService
void this.notifyEvent(FeaturesEvent.FeaturesAvailabilityChanged)
}
public toggleExperimentalFeature(identifier: FeatureIdentifier): void {
public toggleExperimentalFeature(identifier: string): void {
if (this.isExperimentalFeatureEnabled(identifier)) {
this.disableExperimentalFeature(identifier)
} else {
@@ -203,19 +210,19 @@ export class FeaturesService
}
}
public getExperimentalFeatures(): FeatureIdentifier[] {
public getExperimentalFeatures(): string[] {
return ExperimentalFeatures
}
public isExperimentalFeature(featureId: FeatureIdentifier): boolean {
public isExperimentalFeature(featureId: string): boolean {
return this.getExperimentalFeatures().includes(featureId)
}
public getEnabledExperimentalFeatures(): FeatureIdentifier[] {
public getEnabledExperimentalFeatures(): string[] {
return this.enabledExperimentalFeatures
}
public isExperimentalFeatureEnabled(featureId: FeatureIdentifier): boolean {
public isExperimentalFeatureEnabled(featureId: string): boolean {
return this.enabledExperimentalFeatures.includes(featureId)
}
@@ -302,10 +309,10 @@ export class FeaturesService
}
hasPaidAnyPartyOnlineOrOfflineSubscription(): boolean {
return this.onlineRolesIncludePaidSubscription() || this.hasOfflineRepo()
return this.onlineRolesIncludePaidSubscription() || this.hasOfflineRepo() || this.hasFirstPartyOnlineSubscription()
}
private hasFirstPartyOnlineSubscription(): boolean {
hasFirstPartyOnlineSubscription(): boolean {
return this.sessions.isSignedIntoFirstPartyServer() && this.subscriptions.hasOnlineSubscription()
}
@@ -364,12 +371,13 @@ export class FeaturesService
}
public isThirdPartyFeature(identifier: string): boolean {
const isNativeFeature = !!FindNativeFeature(identifier as FeatureIdentifier)
const isNativeFeature = !!FindNativeFeature(identifier)
return !isNativeFeature
}
onlineRolesIncludePaidSubscription(): boolean {
const unpaidRoles = [RoleName.NAMES.CoreUser]
return this.onlineRoles.some((role) => !unpaidRoles.includes(role))
}
@@ -392,7 +400,7 @@ export class FeaturesService
}
public getFeatureStatus(
featureId: FeatureIdentifier,
featureId: NativeFeatureIdentifier | Uuid,
options: { inContextOfItem?: DecryptedItemInterface } = {},
): FeatureStatus {
return this.getFeatureStatusUseCase.execute({

View File

@@ -1,28 +1,23 @@
import { FeatureIdentifier } from '@standardnotes/features'
import { NativeFeatureIdentifier } from '@standardnotes/features'
import { FeatureStatus, ItemManagerInterface } from '@standardnotes/services'
import { GetFeatureStatusUseCase } from './GetFeatureStatus'
import { ComponentInterface, DecryptedItemInterface } from '@standardnotes/models'
jest.mock('@standardnotes/features', () => ({
FeatureIdentifier: {
DarkTheme: 'darkTheme',
},
FindNativeFeature: jest.fn(),
}))
import { FindNativeFeature } from '@standardnotes/features'
import { Subscription } from '@standardnotes/responses'
import { Uuid } from '@standardnotes/domain-core'
describe('GetFeatureStatusUseCase', () => {
let items: jest.Mocked<ItemManagerInterface>
let usecase: GetFeatureStatusUseCase
let findNativeFeature: jest.Mock<any, any>
beforeEach(() => {
items = {
getDisplayableComponents: jest.fn(),
} as unknown as jest.Mocked<ItemManagerInterface>
usecase = new GetFeatureStatusUseCase(items)
;(FindNativeFeature as jest.Mock).mockReturnValue(undefined)
findNativeFeature = jest.fn()
usecase.findNativeFeature = findNativeFeature
findNativeFeature.mockReturnValue(undefined)
})
afterEach(() => {
@@ -33,7 +28,7 @@ describe('GetFeatureStatusUseCase', () => {
it('should return entitled for free features', () => {
expect(
usecase.execute({
featureId: FeatureIdentifier.DarkTheme,
featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.DarkTheme).getValue(),
hasPaidAnyPartyOnlineOrOfflineSubscription: false,
firstPartyOnlineSubscription: undefined,
firstPartyRoles: undefined,
@@ -44,11 +39,11 @@ describe('GetFeatureStatusUseCase', () => {
describe('deprecated features', () => {
it('should return entitled for deprecated paid features if any subscription is active', () => {
;(FindNativeFeature as jest.Mock).mockReturnValue({ deprecated: true })
findNativeFeature.mockReturnValue({ deprecated: true })
expect(
usecase.execute({
featureId: 'deprecatedFeature',
featureId: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
hasPaidAnyPartyOnlineOrOfflineSubscription: true,
firstPartyOnlineSubscription: undefined,
firstPartyRoles: undefined,
@@ -57,11 +52,11 @@ describe('GetFeatureStatusUseCase', () => {
})
it('should return NoUserSubscription for deprecated paid features if no subscription is active', () => {
;(FindNativeFeature as jest.Mock).mockReturnValue({ deprecated: true })
findNativeFeature.mockReturnValue({ deprecated: true })
expect(
usecase.execute({
featureId: 'deprecatedFeature',
featureId: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
hasPaidAnyPartyOnlineOrOfflineSubscription: false,
firstPartyOnlineSubscription: undefined,
firstPartyRoles: undefined,
@@ -72,11 +67,11 @@ describe('GetFeatureStatusUseCase', () => {
describe('native features', () => {
it('should return Entitled if the context item belongs to a shared vault and user does not have subscription', () => {
;(FindNativeFeature as jest.Mock).mockReturnValue({ deprecated: false })
findNativeFeature.mockReturnValue({ deprecated: false })
expect(
usecase.execute({
featureId: 'nativeFeature',
featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.AutobiographyTheme).getValue(),
firstPartyOnlineSubscription: undefined,
firstPartyRoles: undefined,
hasPaidAnyPartyOnlineOrOfflineSubscription: false,
@@ -86,11 +81,11 @@ describe('GetFeatureStatusUseCase', () => {
})
it('should return NoUserSubscription if the context item does not belong to a shared vault and user does not have subscription', () => {
;(FindNativeFeature as jest.Mock).mockReturnValue({ deprecated: false })
findNativeFeature.mockReturnValue({ deprecated: false })
expect(
usecase.execute({
featureId: 'nativeFeature',
featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.AutobiographyTheme).getValue(),
firstPartyOnlineSubscription: undefined,
firstPartyRoles: undefined,
hasPaidAnyPartyOnlineOrOfflineSubscription: false,
@@ -100,11 +95,11 @@ describe('GetFeatureStatusUseCase', () => {
})
it('should return NoUserSubscription for native features without subscription and roles', () => {
;(FindNativeFeature as jest.Mock).mockReturnValue({ deprecated: false })
findNativeFeature.mockReturnValue({ deprecated: false })
expect(
usecase.execute({
featureId: 'nativeFeature',
featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.AutobiographyTheme).getValue(),
firstPartyOnlineSubscription: undefined,
firstPartyRoles: undefined,
hasPaidAnyPartyOnlineOrOfflineSubscription: false,
@@ -113,14 +108,14 @@ describe('GetFeatureStatusUseCase', () => {
})
it('should return NotInCurrentPlan for native features with roles not in available roles', () => {
;(FindNativeFeature as jest.Mock).mockReturnValue({
findNativeFeature.mockReturnValue({
deprecated: false,
availableInRoles: ['notInRole'],
})
expect(
usecase.execute({
featureId: 'nativeFeature',
featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.AutobiographyTheme).getValue(),
firstPartyOnlineSubscription: undefined,
firstPartyRoles: { online: ['inRole'] },
hasPaidAnyPartyOnlineOrOfflineSubscription: false,
@@ -129,14 +124,14 @@ describe('GetFeatureStatusUseCase', () => {
})
it('should return Entitled for native features with roles in available roles and active subscription', () => {
;(FindNativeFeature as jest.Mock).mockReturnValue({
findNativeFeature.mockReturnValue({
deprecated: false,
availableInRoles: ['inRole'],
})
expect(
usecase.execute({
featureId: 'nativeFeature',
featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.AutobiographyTheme).getValue(),
firstPartyOnlineSubscription: {
endsAt: new Date(Date.now() + 10000).getTime(),
} as jest.Mocked<Subscription>,
@@ -147,14 +142,14 @@ describe('GetFeatureStatusUseCase', () => {
})
it('should return InCurrentPlanButExpired for native features with roles in available roles and expired subscription', () => {
;(FindNativeFeature as jest.Mock).mockReturnValue({
findNativeFeature.mockReturnValue({
deprecated: false,
availableInRoles: ['inRole'],
})
expect(
usecase.execute({
featureId: 'nativeFeature',
featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.AutobiographyTheme).getValue(),
firstPartyOnlineSubscription: {
endsAt: new Date(Date.now() - 10000).getTime(),
} as jest.Mocked<Subscription>,
@@ -168,7 +163,7 @@ describe('GetFeatureStatusUseCase', () => {
describe('third party features', () => {
it('should return Entitled for third-party features', () => {
const mockComponent = {
identifier: 'thirdPartyFeature',
uuid: '00000000-0000-0000-0000-000000000000',
isExpired: false,
} as unknown as jest.Mocked<ComponentInterface>
@@ -176,7 +171,7 @@ describe('GetFeatureStatusUseCase', () => {
expect(
usecase.execute({
featureId: 'thirdPartyFeature',
featureId: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
hasPaidAnyPartyOnlineOrOfflineSubscription: false,
firstPartyOnlineSubscription: undefined,
firstPartyRoles: undefined,
@@ -189,7 +184,7 @@ describe('GetFeatureStatusUseCase', () => {
expect(
usecase.execute({
featureId: 'nonExistingThirdPartyFeature',
featureId: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
hasPaidAnyPartyOnlineOrOfflineSubscription: false,
firstPartyOnlineSubscription: undefined,
firstPartyRoles: undefined,
@@ -199,7 +194,7 @@ describe('GetFeatureStatusUseCase', () => {
it('should return InCurrentPlanButExpired for expired third-party features', () => {
const mockComponent = {
identifier: 'thirdPartyFeature',
uuid: '00000000-0000-0000-0000-000000000000',
isExpired: true,
} as unknown as jest.Mocked<ComponentInterface>
@@ -207,7 +202,7 @@ describe('GetFeatureStatusUseCase', () => {
expect(
usecase.execute({
featureId: 'thirdPartyFeature',
featureId: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
hasPaidAnyPartyOnlineOrOfflineSubscription: false,
firstPartyOnlineSubscription: undefined,
firstPartyRoles: undefined,

View File

@@ -1,4 +1,5 @@
import { AnyFeatureDescription, FeatureIdentifier, FindNativeFeature } from '@standardnotes/features'
import { Uuid } from '@standardnotes/domain-core'
import { AnyFeatureDescription, NativeFeatureIdentifier, FindNativeFeature } from '@standardnotes/features'
import { DecryptedItemInterface } from '@standardnotes/models'
import { Subscription } from '@standardnotes/responses'
import { FeatureStatus, ItemManagerInterface } from '@standardnotes/services'
@@ -8,20 +9,19 @@ export class GetFeatureStatusUseCase {
constructor(private items: ItemManagerInterface) {}
execute(dto: {
featureId: FeatureIdentifier | string
featureId: NativeFeatureIdentifier | Uuid
firstPartyOnlineSubscription: Subscription | undefined
firstPartyRoles: { online: string[] } | { offline: string[] } | undefined
hasPaidAnyPartyOnlineOrOfflineSubscription: boolean
inContextOfItem?: DecryptedItemInterface
}): FeatureStatus {
if (this.isFreeFeature(dto.featureId as FeatureIdentifier)) {
if (this.isFreeFeature(dto.featureId)) {
return FeatureStatus.Entitled
}
const nativeFeature = FindNativeFeature(dto.featureId as FeatureIdentifier)
const nativeFeature = this.findNativeFeature(dto.featureId)
if (!nativeFeature) {
return this.getThirdPartyFeatureStatus(dto.featureId as string)
return this.getThirdPartyFeatureStatus(dto.featureId)
}
if (nativeFeature.deprecated) {
@@ -39,6 +39,10 @@ export class GetFeatureStatusUseCase {
})
}
findNativeFeature(featureId: NativeFeatureIdentifier | Uuid): AnyFeatureDescription | undefined {
return FindNativeFeature(featureId.value)
}
private getDeprecatedNativeFeatureStatus(dto: {
hasPaidAnyPartyOnlineOrOfflineSubscription: boolean
nativeFeature: AnyFeatureDescription
@@ -95,8 +99,8 @@ export class GetFeatureStatusUseCase {
return FeatureStatus.Entitled
}
private getThirdPartyFeatureStatus(featureId: string): FeatureStatus {
const component = this.items.getDisplayableComponents().find((candidate) => candidate.identifier === featureId)
private getThirdPartyFeatureStatus(uuid: Uuid): FeatureStatus {
const component = this.items.getDisplayableComponents().find((candidate) => candidate.uuid === uuid.value)
if (!component) {
return FeatureStatus.NoUserSubscription
@@ -109,7 +113,9 @@ export class GetFeatureStatusUseCase {
return FeatureStatus.Entitled
}
private isFreeFeature(featureId: FeatureIdentifier) {
return [FeatureIdentifier.DarkTheme, FeatureIdentifier.PlainEditor].includes(featureId)
private isFreeFeature(featureId: NativeFeatureIdentifier) {
return [NativeFeatureIdentifier.TYPES.DarkTheme, NativeFeatureIdentifier.TYPES.PlainEditor].includes(
featureId.value,
)
}
}