refactor: offline roles (#2169)

This commit is contained in:
Mo
2023-01-19 21:46:21 -06:00
committed by GitHub
parent 391b2af4e1
commit 544a28d450
33 changed files with 282 additions and 266 deletions

View File

@@ -1160,8 +1160,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
return this.apiService.isThirdPartyHostUsed()
}
public getCloudProviderIntegrationUrl(cloudProviderName: Settings.CloudProvider, isDevEnvironment: boolean): string {
return this.settingsService.getCloudProviderIntegrationUrl(cloudProviderName, isDevEnvironment)
public getCloudProviderIntegrationUrl(cloudProviderName: Settings.CloudProvider): string {
return this.settingsService.getCloudProviderIntegrationUrl(cloudProviderName)
}
private constructServices() {

View File

@@ -1,24 +1,15 @@
export const APPLICATION_DEFAULT_HOSTS = [
'api.standardnotes.com',
'api-dev.standardnotes.com',
'sync.standardnotes.org',
'syncing-server-demo.standardnotes.com',
]
export const APPLICATION_DEFAULT_HOSTS = ['api.standardnotes.com', 'sync.standardnotes.org']
export const FILES_DEFAULT_HOSTS = ['files.standardnotes.com', 'files-dev.standardnotes.com']
export const FILES_DEFAULT_HOSTS = ['files.standardnotes.com']
export const TRUSTED_FEATURE_HOSTS = [
'api-dev.standardnotes.com',
'api.standardnotes.com',
'extensions.standardnotes.com',
'extensions.standardnotes.org',
'extensions-server-dev.standardnotes.org',
'extensions-server-dev.standardnotes.com',
'features.standardnotes.com',
]
export enum ExtensionsServerURL {
Dev = 'https://extensions-server-dev.standardnotes.org',
Prod = 'https://extensions.standardnotes.org',
}

View File

@@ -654,7 +654,7 @@ export class SNApiService
public async downloadOfflineFeaturesFromRepo(
repo: SNFeatureRepo,
): Promise<{ features: FeatureDescription[] } | ClientDisplayableError> {
): Promise<{ features: FeatureDescription[]; roles: string[] } | ClientDisplayableError> {
try {
const featuresUrl = repo.offlineFeaturesUrl
const extensionKey = repo.offlineKey
@@ -678,8 +678,10 @@ export class SNApiService
if (response.error) {
return ClientDisplayableError.FromError(response.error)
}
const data = (response as Responses.GetOfflineFeaturesResponse).data
return {
features: (response as Responses.GetOfflineFeaturesResponse).data?.features || [],
features: data?.features || [],
roles: data?.roles || [],
}
} catch {
return new ClientDisplayableError(API_MESSAGE_FAILED_OFFLINE_ACTIVATION)

View File

@@ -80,7 +80,7 @@ export class SNWebSocketsService extends AbstractService<WebSocketsServiceEvent,
return response.data.token
} catch (error) {
console.error((error as Error).message)
console.error('Caught error:', (error as Error).message)
return undefined
}

View File

@@ -170,7 +170,7 @@ describe('featuresService', () => {
featuresService.getExperimentalFeatures = jest.fn().mockReturnValue([FeatureIdentifier.PlusEditor])
featuresService.initializeFromDisk()
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
expect(itemManager.createItem).not.toHaveBeenCalled()
})
@@ -191,7 +191,7 @@ describe('featuresService', () => {
featuresService.getEnabledExperimentalFeatures = jest.fn().mockReturnValue([FeatureIdentifier.PlusEditor])
featuresService.initializeFromDisk()
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
expect(itemManager.createItem).toHaveBeenCalled()
})
})
@@ -213,7 +213,7 @@ describe('featuresService', () => {
const mock = (featuresService['notifyEvent'] = jest.fn())
const newRoles = [...roles, RoleName.NAMES.PlusUser]
await featuresService.setRoles(newRoles)
await featuresService.setOnlineRoles(newRoles)
expect(mock.mock.calls[0][0]).toEqual(FeaturesEvent.UserRolesChanged)
})
@@ -226,7 +226,7 @@ describe('featuresService', () => {
const spy = jest.spyOn(featuresService, 'notifyEvent' as never)
const newRoles = [...roles, RoleName.NAMES.ProUser]
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
expect(spy.mock.calls[2][0]).toEqual(FeaturesEvent.DidPurchaseSubscription)
})
@@ -235,12 +235,12 @@ describe('featuresService', () => {
storageService.getValue = jest.fn().mockReturnValue(roles)
const featuresService = createService()
featuresService.initializeFromDisk()
featuresService['roles'] = []
featuresService['onlineRoles'] = []
const spy = jest.spyOn(featuresService, 'notifyEvent' as never)
const newRoles = [...roles, RoleName.NAMES.ProUser]
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
const triggeredEvents = spy.mock.calls.map((call) => call[0])
expect(triggeredEvents).not.toContain(FeaturesEvent.DidPurchaseSubscription)
@@ -252,7 +252,7 @@ describe('featuresService', () => {
storageService.getValue = jest.fn().mockReturnValue(roles)
const featuresService = createService()
featuresService.initializeFromDisk()
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
expect(storageService.setValue).toHaveBeenCalledWith(StorageKey.UserRoles, newRoles)
expect(apiService.getUserFeatures).toHaveBeenCalledWith('123')
})
@@ -263,7 +263,7 @@ describe('featuresService', () => {
storageService.getValue = jest.fn().mockReturnValue(roles)
const featuresService = createService()
featuresService.initializeFromDisk()
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
expect(storageService.setValue).toHaveBeenCalledWith(StorageKey.UserRoles, newRoles)
expect(apiService.getUserFeatures).toHaveBeenCalledWith('123')
})
@@ -274,7 +274,7 @@ describe('featuresService', () => {
storageService.getValue = jest.fn().mockReturnValue(roles)
const featuresService = createService()
featuresService.initializeFromDisk()
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
expect(storageService.setValue).toHaveBeenCalledWith(StorageKey.UserFeatures, features)
})
@@ -284,7 +284,7 @@ describe('featuresService', () => {
storageService.getValue = jest.fn().mockReturnValue(roles)
const featuresService = createService()
featuresService.initializeFromDisk()
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
expect(itemManager.createItem).toHaveBeenCalledTimes(2)
expect(itemManager.createItem).toHaveBeenCalledWith(
ContentType.Theme,
@@ -328,7 +328,7 @@ describe('featuresService', () => {
itemManager.getItems = jest.fn().mockReturnValue([existingItem])
const featuresService = createService()
featuresService.initializeFromDisk()
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
expect(itemManager.changeComponent).toHaveBeenCalledWith(existingItem, expect.any(Function))
})
@@ -354,7 +354,7 @@ describe('featuresService', () => {
const featuresService = createService()
featuresService.initializeFromDisk()
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
expect(itemManager.createItem).toHaveBeenCalledWith(
ContentType.Component,
expect.objectContaining({
@@ -401,7 +401,7 @@ describe('featuresService', () => {
const featuresService = createService()
featuresService.initializeFromDisk()
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
expect(itemManager.setItemsToBeDeleted).toHaveBeenCalledWith([existingItem])
})
@@ -424,7 +424,7 @@ describe('featuresService', () => {
storageService.getValue = jest.fn().mockReturnValue(roles)
const featuresService = createService()
featuresService.initializeFromDisk()
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
expect(itemManager.createItem).not.toHaveBeenCalled()
})
@@ -447,7 +447,7 @@ describe('featuresService', () => {
storageService.getValue = jest.fn().mockReturnValue(roles)
const featuresService = createService()
featuresService.initializeFromDisk()
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
expect(itemManager.createItem).not.toHaveBeenCalled()
})
@@ -455,10 +455,10 @@ describe('featuresService', () => {
storageService.getValue = jest.fn().mockReturnValue(roles)
const featuresService = createService()
featuresService.initializeFromDisk()
await featuresService.updateRolesAndFetchFeatures('123', roles)
await featuresService.updateRolesAndFetchFeatures('123', roles)
await featuresService.updateRolesAndFetchFeatures('123', roles)
await featuresService.updateRolesAndFetchFeatures('123', roles)
await featuresService.updateOnlineRolesAndFetchFeatures('123', roles)
await featuresService.updateOnlineRolesAndFetchFeatures('123', roles)
await featuresService.updateOnlineRolesAndFetchFeatures('123', roles)
await featuresService.updateOnlineRolesAndFetchFeatures('123', roles)
expect(storageService.setValue).toHaveBeenCalledTimes(2)
})
@@ -482,7 +482,7 @@ describe('featuresService', () => {
const nativeFeature = featuresService['mapRemoteNativeFeatureToStaticFeature'](remoteFeature)
featuresService['mapRemoteNativeFeatureToItem'] = jest.fn()
featuresService.initializeFromDisk()
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
expect(featuresService['mapRemoteNativeFeatureToItem']).toHaveBeenCalledWith(
nativeFeature,
expect.anything(),
@@ -509,7 +509,26 @@ describe('featuresService', () => {
await expect(() => featuresService['mapRemoteNativeFeatureToItem'](clientFeature, [], [])).rejects.toThrow()
})
it('feature status', async () => {
it('role-based feature status', async () => {
const featuresService = createService()
features = [] as jest.Mocked<FeatureDescription[]>
apiService.getUserFeatures = jest.fn().mockReturnValue({
data: {
features,
},
})
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled)
expect(featuresService.getFeatureStatus(FeatureIdentifier.SuperEditor)).toBe(FeatureStatus.Entitled)
})
it('feature status with no paid role but features listings', async () => {
const featuresService = createService()
features = [
@@ -535,54 +554,21 @@ describe('featuresService', () => {
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled)
expect(featuresService.getFeatureStatus(FeatureIdentifier.PlusEditor)).toBe(FeatureStatus.NotInCurrentPlan)
expect(featuresService.getFeatureStatus(FeatureIdentifier.SheetsEditor)).toBe(FeatureStatus.NotInCurrentPlan)
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.NoUserSubscription)
expect(featuresService.getFeatureStatus(FeatureIdentifier.PlusEditor)).toBe(FeatureStatus.NoUserSubscription)
expect(featuresService.getFeatureStatus(FeatureIdentifier.SheetsEditor)).toBe(FeatureStatus.NoUserSubscription)
features = [
{
identifier: FeatureIdentifier.MidnightTheme,
content_type: ContentType.Theme,
expires_at: expiredDate,
role_name: RoleName.NAMES.PlusUser,
},
{
identifier: FeatureIdentifier.PlusEditor,
content_type: ContentType.Component,
expires_at: expiredDate,
role_name: RoleName.NAMES.ProUser,
},
] as jest.Mocked<FeatureDescription[]>
apiService.getUserFeatures = jest.fn().mockReturnValue({
data: {
features,
},
})
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.PlusUser])
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(
FeatureStatus.InCurrentPlanButExpired,
)
expect(featuresService.getFeatureStatus(FeatureIdentifier.PlusEditor)).toBe(FeatureStatus.NotInCurrentPlan)
expect(featuresService.getFeatureStatus(FeatureIdentifier.SheetsEditor)).toBe(FeatureStatus.NotInCurrentPlan)
})
it('availableInRoles-based features', async () => {
it('role-based features while not signed into first party server', async () => {
const featuresService = createService()
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.ProUser])
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false)
expect(featuresService.getFeatureStatus(FeatureIdentifier.SuperEditor)).toBe(FeatureStatus.Entitled)
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.ProUser])
expect(featuresService.getFeatureStatus(FeatureIdentifier.SuperEditor)).toBe(FeatureStatus.NotInCurrentPlan)
})
it('third party feature status', async () => {
@@ -629,7 +615,7 @@ describe('featuresService', () => {
} as never),
])
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
expect(featuresService.getFeatureStatus(themeFeature.identifier)).toBe(FeatureStatus.Entitled)
expect(featuresService.getFeatureStatus(editorFeature.identifier)).toBe(FeatureStatus.InCurrentPlanButExpired)
@@ -641,7 +627,7 @@ describe('featuresService', () => {
it('feature status should be not entitled if no account or offline repo', async () => {
const featuresService = createService()
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false)
@@ -653,30 +639,6 @@ describe('featuresService', () => {
)
})
it('feature status should be entitled for subscriber until first successful features request made if no cached features', async () => {
const featuresService = createService()
apiService.getUserFeatures = jest.fn().mockReturnValue({
data: {
features: [],
},
})
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
featuresService['completedSuccessfulFeaturesRetrieval'] = false
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled)
expect(featuresService.getFeatureStatus(FeatureIdentifier.TokenVaultEditor)).toBe(FeatureStatus.Entitled)
await featuresService.didDownloadFeatures(features)
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled)
expect(featuresService.getFeatureStatus(FeatureIdentifier.TokenVaultEditor)).toBe(FeatureStatus.NotInCurrentPlan)
})
it('didDownloadFeatures should filter out client controlled features', async () => {
const featuresService = createService()
@@ -687,31 +649,13 @@ describe('featuresService', () => {
expect(featuresService['mapRemoteNativeFeaturesToItems']).toHaveBeenCalledWith([])
})
it('feature status should be dynamic for subscriber if cached features and no successful features request made yet', async () => {
const featuresService = createService()
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
featuresService['completedSuccessfulFeaturesRetrieval'] = false
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled)
expect(featuresService.getFeatureStatus(FeatureIdentifier.TokenVaultEditor)).toBe(FeatureStatus.NotInCurrentPlan)
featuresService['completedSuccessfulFeaturesRetrieval'] = false
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled)
expect(featuresService.getFeatureStatus(FeatureIdentifier.TokenVaultEditor)).toBe(FeatureStatus.NotInCurrentPlan)
})
it('feature status for offline subscription', async () => {
const featuresService = createService()
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false)
featuresService.rolesIncludePaidSubscription = jest.fn().mockReturnValue(false)
featuresService.onlineRolesIncludePaidSubscription = jest.fn().mockReturnValue(false)
featuresService['completedSuccessfulFeaturesRetrieval'] = true
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.NoUserSubscription)
@@ -720,9 +664,11 @@ describe('featuresService', () => {
)
featuresService.hasOfflineRepo = jest.fn().mockReturnValue(true)
featuresService.hasFirstPartySubscription = jest.fn().mockReturnValue(true)
await featuresService.setOfflineRoles([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled)
expect(featuresService.getFeatureStatus(FeatureIdentifier.TokenVaultEditor)).toBe(FeatureStatus.NotInCurrentPlan)
expect(featuresService.getFeatureStatus(FeatureIdentifier.TokenVaultEditor)).toBe(FeatureStatus.Entitled)
})
it('feature status for deprecated feature', async () => {
@@ -734,7 +680,7 @@ describe('featuresService', () => {
FeatureStatus.NoUserSubscription,
)
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
expect(featuresService.getFeatureStatus(FeatureIdentifier.DeprecatedFileSafe as FeatureIdentifier)).toBe(
FeatureStatus.Entitled,
@@ -744,25 +690,25 @@ describe('featuresService', () => {
it('has paid subscription', async () => {
const featuresService = createService()
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
expect(featuresService.hasPaidOnlineOrOfflineSubscription()).toBeFalsy
expect(featuresService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toBeFalsy
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
expect(featuresService.hasPaidOnlineOrOfflineSubscription()).toEqual(true)
expect(featuresService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true)
})
it('has paid subscription should be true if offline repo and signed into third party server', async () => {
const featuresService = createService()
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
featuresService.hasOfflineRepo = jest.fn().mockReturnValue(true)
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false)
expect(featuresService.hasPaidOnlineOrOfflineSubscription()).toEqual(true)
expect(featuresService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true)
})
})
@@ -829,7 +775,11 @@ describe('featuresService', () => {
it('should sort given roles according to role hierarchy', () => {
const featuresService = createService()
const sortedRoles = featuresService.rolesBySorting([RoleName.NAMES.ProUser, RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
const sortedRoles = featuresService.rolesBySorting([
RoleName.NAMES.ProUser,
RoleName.NAMES.CoreUser,
RoleName.NAMES.PlusUser,
])
expect(sortedRoles).toStrictEqual([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser])
})
@@ -839,7 +789,7 @@ describe('featuresService', () => {
it('should be false if core user checks for plus role', async () => {
const featuresService = createService()
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
const hasPlusUserRole = featuresService.hasMinimumRole(RoleName.NAMES.PlusUser)
@@ -849,7 +799,9 @@ describe('featuresService', () => {
it('should be false if plus user checks for pro role', async () => {
const featuresService = createService()
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.PlusUser, RoleName.NAMES.CoreUser])
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.PlusUser, RoleName.NAMES.CoreUser])
const hasProUserRole = featuresService.hasMinimumRole(RoleName.NAMES.ProUser)
@@ -859,7 +811,9 @@ describe('featuresService', () => {
it('should be true if pro user checks for core user', async () => {
const featuresService = createService()
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser])
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser])
const hasCoreUserRole = featuresService.hasMinimumRole(RoleName.NAMES.CoreUser)
@@ -869,7 +823,9 @@ describe('featuresService', () => {
it('should be true if pro user checks for pro user', async () => {
const featuresService = createService()
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser])
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser])
const hasProUserRole = featuresService.hasMinimumRole(RoleName.NAMES.ProUser)

View File

@@ -58,7 +58,8 @@ export class SNFeaturesService
implements FeaturesClientInterface, InternalEventHandlerInterface
{
private deinited = false
private roles: string[] = []
private onlineRoles: string[] = []
private offlineRoles: string[] = []
private features: FeaturesImports.FeatureDescription[] = []
private enabledExperimentalFeatures: FeaturesImports.FeatureIdentifier[] = []
private removeWebSocketsServiceObserver: () => void
@@ -87,7 +88,7 @@ export class SNFeaturesService
const {
payload: { userUuid, currentRoles },
} = data as UserRolesChangedEvent
await this.updateRolesAndFetchFeatures(userUuid, currentRoles)
await this.updateOnlineRolesAndFetchFeatures(userUuid, currentRoles)
}
})
@@ -124,6 +125,16 @@ export class SNFeaturesService
})
}
public initializeFromDisk(): void {
this.onlineRoles = this.storageService.getValue<string[]>(StorageKey.UserRoles, undefined, [])
this.offlineRoles = this.storageService.getValue<string[]>(StorageKey.OfflineUserRoles, undefined, [])
this.features = this.storageService.getValue(StorageKey.UserFeatures, undefined, [])
this.enabledExperimentalFeatures = this.storageService.getValue(StorageKey.ExperimentalFeatures, undefined, [])
}
async handleEvent(event: InternalEventInterface): Promise<void> {
if (event.type === ApiServiceEvent.MetaReceived) {
if (!this.syncService) {
@@ -142,7 +153,7 @@ export class SNFeaturesService
}
const { userUuid, userRoles } = event.payload as MetaReceivedData
await this.updateRolesAndFetchFeatures(
await this.updateOnlineRolesAndFetchFeatures(
userUuid,
userRoles.map((role) => role.name),
)
@@ -155,7 +166,7 @@ export class SNFeaturesService
if (stage === ApplicationStage.FullSyncCompleted_13) {
void this.mapClientControlledFeaturesToItems()
if (!this.rolesIncludePaidSubscription()) {
if (!this.hasFirstPartyOnlineSubscription()) {
const offlineRepo = this.getOfflineRepo()
if (offlineRepo) {
void this.downloadOfflineFeatures(offlineRepo)
@@ -194,7 +205,7 @@ export class SNFeaturesService
}
public enableExperimentalFeature(identifier: FeaturesImports.FeatureIdentifier): void {
const feature = this.getUserFeature(identifier)
const feature = this.getFeatureThatOriginallyCameFromServer(identifier)
this.enabledExperimentalFeatures.push(identifier)
@@ -309,10 +320,14 @@ export class SNFeaturesService
repo: Models.SNFeatureRepo,
): Promise<SetOfflineFeaturesFunctionResponse | ClientDisplayableError> {
const result = await this.apiService.downloadOfflineFeaturesFromRepo(repo)
if (result instanceof ClientDisplayableError) {
return result
}
await this.didDownloadFeatures(result.features)
await this.setOfflineRoles(result.roles)
return undefined
}
@@ -363,18 +378,29 @@ export class SNFeaturesService
}
}
public initializeFromDisk(): void {
this.roles = this.storageService.getValue<string[]>(StorageKey.UserRoles, undefined, [])
this.features = this.storageService.getValue(StorageKey.UserFeatures, undefined, [])
this.enabledExperimentalFeatures = this.storageService.getValue(StorageKey.ExperimentalFeatures, undefined, [])
hasFirstPartyOnlineSubscription(): boolean {
return this.sessionManager.isSignedIntoFirstPartyServer() && this.onlineRolesIncludePaidSubscription()
}
public async updateRolesAndFetchFeatures(userUuid: UuidString, roles: string[]): Promise<void> {
const previousRoles = this.roles
hasFirstPartySubscription(): boolean {
if (this.hasFirstPartyOnlineSubscription()) {
return true
}
const userRolesChanged = this.haveRolesChanged(roles)
const offlineRepo = this.getOfflineRepo()
if (!offlineRepo) {
return false
}
const hasFirstPartyOfflineSubscription = offlineRepo.content.offlineFeaturesUrl === PROD_OFFLINE_FEATURES_URL
return hasFirstPartyOfflineSubscription
}
async updateOnlineRolesAndFetchFeatures(userUuid: UuidString, roles: string[]): Promise<void> {
const previousRoles = this.onlineRoles
const userRolesChanged =
roles.some((role) => !this.onlineRoles.includes(role)) || this.onlineRoles.some((role) => !roles.includes(role))
const isInitialLoadRolesChange = previousRoles.length === 0 && userRolesChanged
@@ -384,7 +410,7 @@ export class SNFeaturesService
this.needsInitialFeaturesUpdate = false
await this.setRoles(roles)
await this.setOnlineRoles(roles)
const shouldDownloadRoleBasedFeatures = !this.hasOfflineRepo()
@@ -398,22 +424,34 @@ export class SNFeaturesService
}
if (userRolesChanged && !isInitialLoadRolesChange) {
if (this.rolesIncludePaidSubscription()) {
if (this.onlineRolesIncludePaidSubscription()) {
await this.notifyEvent(FeaturesEvent.DidPurchaseSubscription)
}
}
}
async setRoles(roles: string[]): Promise<void> {
const rolesChanged = !arraysEqual(this.roles, roles)
async setOnlineRoles(roles: string[]): Promise<void> {
const rolesChanged = !arraysEqual(this.onlineRoles, roles)
this.roles = roles
this.onlineRoles = roles
if (rolesChanged) {
void this.notifyEvent(FeaturesEvent.UserRolesChanged)
}
this.storageService.setValue(StorageKey.UserRoles, this.roles)
this.storageService.setValue(StorageKey.UserRoles, this.onlineRoles)
}
async setOfflineRoles(roles: string[]): Promise<void> {
const rolesChanged = !arraysEqual(this.offlineRoles, roles)
this.offlineRoles = roles
if (rolesChanged) {
void this.notifyEvent(FeaturesEvent.UserRolesChanged)
}
this.storageService.setValue(StorageKey.OfflineUserRoles, this.offlineRoles)
}
public async didDownloadFeatures(features: FeaturesImports.FeatureDescription[]): Promise<void> {
@@ -465,17 +503,19 @@ export class SNFeaturesService
return nativeFeatureCopy
}
public getUserFeature(featureId: FeaturesImports.FeatureIdentifier): FeaturesImports.FeatureDescription | undefined {
public getFeatureThatOriginallyCameFromServer(
featureId: FeaturesImports.FeatureIdentifier,
): FeaturesImports.FeatureDescription | undefined {
return this.features.find((feature) => feature.identifier === featureId)
}
rolesIncludePaidSubscription(): boolean {
onlineRolesIncludePaidSubscription(): boolean {
const unpaidRoles = [RoleName.NAMES.CoreUser]
return this.roles.some((role) => !unpaidRoles.includes(role))
return this.onlineRoles.some((role) => !unpaidRoles.includes(role))
}
public hasPaidOnlineOrOfflineSubscription(): boolean {
return this.rolesIncludePaidSubscription() || this.hasOfflineRepo()
hasPaidAnyPartyOnlineOrOfflineSubscription(): boolean {
return this.onlineRolesIncludePaidSubscription() || this.hasOfflineRepo()
}
public rolesBySorting(roles: string[]): string[] {
@@ -485,7 +525,7 @@ export class SNFeaturesService
public hasMinimumRole(role: string): boolean {
const sortedAllRoles = Object.values(RoleName.NAMES)
const sortedUserRoles = this.rolesBySorting(this.roles)
const sortedUserRoles = this.rolesBySorting(this.rolesToUseForFeatureCheck())
const highestUserRoleIndex = sortedAllRoles.indexOf(lastElement(sortedUserRoles) as string)
@@ -508,16 +548,10 @@ export class SNFeaturesService
}
const nativeFeature = FeaturesImports.FindNativeFeature(featureId)
if (nativeFeature && nativeFeature.availableInRoles) {
const hasRole = this.roles.some((role) => nativeFeature.availableInRoles?.includes(role))
if (hasRole) {
return FeatureStatus.Entitled
}
}
const isDeprecated = this.isFeatureDeprecated(featureId)
if (isDeprecated) {
if (this.hasPaidOnlineOrOfflineSubscription()) {
if (this.hasPaidAnyPartyOnlineOrOfflineSubscription()) {
return FeatureStatus.Entitled
} else {
return FeatureStatus.NoUserSubscription
@@ -538,7 +572,7 @@ export class SNFeaturesService
return FeatureStatus.Entitled
}
if (this.hasPaidOnlineOrOfflineSubscription()) {
if (this.hasPaidAnyPartyOnlineOrOfflineSubscription()) {
if (!this.completedSuccessfulFeaturesRetrieval) {
const hasCachedFeatures = this.features.length > 0
const temporarilyAllowUntilServerUpdates = !hasCachedFeatures
@@ -550,25 +584,27 @@ export class SNFeaturesService
return FeatureStatus.NoUserSubscription
}
const feature = this.getUserFeature(featureId)
if (!feature) {
return FeatureStatus.NotInCurrentPlan
}
const expired = feature.expires_at && new Date(feature.expires_at).getTime() < new Date().getTime()
if (expired) {
if (!this.roles.includes(feature.role_name as string)) {
if (nativeFeature) {
if (!this.hasFirstPartySubscription()) {
return FeatureStatus.NotInCurrentPlan
} else {
return FeatureStatus.InCurrentPlanButExpired
}
const roles = this.rolesToUseForFeatureCheck()
if (nativeFeature.availableInRoles) {
const hasRole = roles.some((role) => {
return nativeFeature.availableInRoles?.includes(role)
})
if (!hasRole) {
return FeatureStatus.NotInCurrentPlan
}
}
}
return FeatureStatus.Entitled
}
private haveRolesChanged(roles: string[]): boolean {
return roles.some((role) => !this.roles.includes(role)) || this.roles.some((role) => !roles.includes(role))
private rolesToUseForFeatureCheck(): string[] {
return this.hasFirstPartyOnlineSubscription() ? this.onlineRoles : this.offlineRoles
}
private componentContentForNativeFeatureDescription(feature: FeaturesImports.FeatureDescription): Models.ItemContent {
@@ -776,7 +812,8 @@ export class SNFeaturesService
;(this.removeWebSocketsServiceObserver as unknown) = undefined
this.removefeatureReposObserver()
;(this.removefeatureReposObserver as unknown) = undefined
;(this.roles as unknown) = undefined
;(this.onlineRoles as unknown) = undefined
;(this.offlineRoles as unknown) = undefined
;(this.storageService as unknown) = undefined
;(this.apiService as unknown) = undefined
;(this.itemManager as unknown) = undefined
@@ -793,7 +830,7 @@ export class SNFeaturesService
override getDiagnostics(): Promise<DiagnosticInfo | undefined> {
return Promise.resolve({
features: {
roles: this.roles,
roles: this.onlineRoles,
features: this.features,
enabledExperimentalFeatures: this.enabledExperimentalFeatures,
needsInitialFeaturesUpdate: this.needsInitialFeaturesUpdate,

View File

@@ -66,10 +66,9 @@ export class SNSettingsService extends AbstractService implements SettingsClient
return this.frequencyOptionsLabels[frequency]
}
getCloudProviderIntegrationUrl(cloudProviderName: CloudProvider, isDevEnvironment: boolean): string {
const { Dev, Prod } = ExtensionsServerURL
const extServerUrl = isDevEnvironment ? Dev : Prod
return `${extServerUrl}/${this.cloudProviderIntegrationUrlEndpoints[cloudProviderName]}?redirect_url=${extServerUrl}/components/cloudlink?`
getCloudProviderIntegrationUrl(cloudProviderName: CloudProvider): string {
const { Prod } = ExtensionsServerURL
return `${Prod}/${this.cloudProviderIntegrationUrlEndpoints[cloudProviderName]}?redirect_url=${Prod}/components/cloudlink?`
}
override deinit(): void {

View File

@@ -60,8 +60,8 @@ describe('features', () => {
describe('new user roles received on api response meta', () => {
it('should save roles and features', async () => {
expect(application.featuresService.roles).to.have.lengthOf(1)
expect(application.featuresService.roles[0]).to.equal('CORE_USER')
expect(application.featuresService.onlineRoles).to.have.lengthOf(1)
expect(application.featuresService.onlineRoles[0]).to.equal('CORE_USER')
expect(application.featuresService.features).to.have.lengthOf(3)
expect(application.featuresService.features[0]).to.containSubset(midnightThemeFeature)
@@ -115,7 +115,7 @@ describe('features', () => {
// Wipe items from initial sync
await application.itemManager.removeAllItemsFromMemory()
// Wipe roles from initial sync
await application.featuresService.setRoles([])
await application.featuresService.setOnlineRoles([])
// Create pre-existing item for theme without all the info
await application.itemManager.createItem(
ContentType.Theme,
@@ -165,7 +165,7 @@ describe('features', () => {
.find((theme) => theme.identifier === midnightThemeFeature.identifier)
// Wipe roles from initial sync
await application.featuresService.setRoles([])
await application.featuresService.setOnlineRoles([])
// Call sync intentionally to get roles again in meta
await application.sync.sync()
@@ -184,7 +184,7 @@ describe('features', () => {
})
it('should provide feature', async () => {
const feature = application.features.getUserFeature(FeatureIdentifier.PlusEditor)
const feature = application.features.getFeatureThatOriginallyCameFromServer(FeatureIdentifier.PlusEditor)
expect(feature).to.containSubset(plusEditorFeature)
})

View File

@@ -645,6 +645,8 @@ describe('keys', function () {
expect(Object.keys(clientBUndecryptables).length).to.equal(1)
expect(Object.keys(clientAUndecryptables).length).to.equal(0)
await contextB.deinit()
})
describe('changing password on 003 client while signed into 004 client should', function () {

View File

@@ -40,7 +40,7 @@ describe('online conflict handling', function () {
afterEach(async function () {
if (!this.application.dealloced) {
await Factory.safeDeinit(this.application)
await this.context.deinit()
}
localStorage.clear()
})
@@ -950,6 +950,7 @@ describe('online conflict handling', function () {
expect(contextA.findNoteByTitle('title-B').payload.updated_at_timestamp).to.equal(noteBExpectedTimestamp)
await this.sharedFinalAssertions()
await contextB.deinit()
}).timeout(20000)
it('editing original note many times after conflict on other client should only result in 2 cumulative notes', async function () {
@@ -979,5 +980,6 @@ describe('online conflict handling', function () {
expect(contextB.noteCount).to.equal(2)
await this.sharedFinalAssertions()
await contextB.deinit()
}).timeout(20000)
})

View File

@@ -1052,5 +1052,7 @@ describe('online syncing', function () {
await contextB.sync()
expect(contextB.application.items.allCountableNotesCount()).to.equal(0)
await contextB.deinit()
})
})