From 544a28d450f276684697439c754d3bc4d3ed3062 Mon Sep 17 00:00:00 2001 From: Mo Date: Thu, 19 Jan 2023 21:46:21 -0600 Subject: [PATCH] refactor: offline roles (#2169) --- .../src/Domain/Feature/FeatureIdentifier.ts | 2 - .../features/src/Domain/Feature/Features.ts | 11 +- .../src/Domain/Lists/ClientFeatures.ts | 42 ++-- .../src/Domain/Lists/DeprecatedFeatures.ts | 6 + packages/features/src/Domain/Lists/Editors.ts | 8 + .../src/Domain/Lists/ServerFeatures.ts | 6 + packages/features/src/Domain/Lists/Themes.ts | 7 + .../Domain/User/GetOfflineFeaturesResponse.ts | 1 + .../Domain/Feature/FeaturesClientInterface.ts | 6 +- .../src/Domain/Storage/StorageKeys.ts | 1 + packages/snjs/lib/Application/Application.ts | 4 +- packages/snjs/lib/Hosts.ts | 13 +- packages/snjs/lib/Services/Api/ApiService.ts | 6 +- .../lib/Services/Api/WebsocketsService.ts | 2 +- .../Services/Features/FeaturesService.spec.ts | 194 +++++++----------- .../lib/Services/Features/FeaturesService.ts | 133 +++++++----- .../Services/Settings/SNSettingsService.ts | 7 +- packages/snjs/mocha/features.test.js | 10 +- packages/snjs/mocha/keys.test.js | 2 + .../snjs/mocha/sync_tests/conflicting.test.js | 4 +- packages/snjs/mocha/sync_tests/online.test.js | 2 + packages/web/CHANGELOG.md | 1 - packages/web/CHANGELOG.md.json | 6 +- .../Components/Footer/UpgradeNow.tsx | 11 +- .../Preferences/Panes/Account/Email.tsx | 2 +- .../Account/Subscription/Subscription.tsx | 4 +- .../CloudBackups/CloudBackupProvider.tsx | 4 +- .../Packages/Provider/PackageProvider.ts | 2 +- .../Subviews/UpgradePrompt.tsx | 7 +- .../Controllers/FeaturesController.ts | 1 + .../Controllers/LinkingController.tsx | 2 +- .../Subscription/SubscriptionController.ts | 37 +++- .../Utils/createEditorMenuGroups.ts | 4 +- 33 files changed, 282 insertions(+), 266 deletions(-) diff --git a/packages/features/src/Domain/Feature/FeatureIdentifier.ts b/packages/features/src/Domain/Feature/FeatureIdentifier.ts index 990fee0b5..ea1bb4fec 100644 --- a/packages/features/src/Domain/Feature/FeatureIdentifier.ts +++ b/packages/features/src/Domain/Feature/FeatureIdentifier.ts @@ -1,5 +1,4 @@ export enum FeatureIdentifier { - AccountSwitcher = 'com.standardnotes.account-switcher', CloudLink = 'org.standardnotes.cloudlink', DailyDropboxBackup = 'org.standardnotes.daily-dropbox-backup', DailyEmailBackup = 'org.standardnotes.daily-email-backup', @@ -21,7 +20,6 @@ export enum FeatureIdentifier { AutobiographyTheme = 'org.standardnotes.theme-autobiography', DynamicTheme = 'org.standardnotes.theme-dynamic', DarkTheme = 'org.standardnotes.theme-focus', - FocusMode = 'org.standardnotes.focus-mode', FuturaTheme = 'org.standardnotes.theme-futura', MidnightTheme = 'org.standardnotes.theme-midnight', SolarizedDarkTheme = 'org.standardnotes.theme-solarized-dark', diff --git a/packages/features/src/Domain/Feature/Features.ts b/packages/features/src/Domain/Feature/Features.ts index 38465be7f..b457d5bf8 100644 --- a/packages/features/src/Domain/Feature/Features.ts +++ b/packages/features/src/Domain/Feature/Features.ts @@ -1,7 +1,5 @@ import { FeatureDescription } from './FeatureDescription' import { FeatureIdentifier } from './FeatureIdentifier' -import { editors } from '../Lists/Editors' -import { themes } from '../Lists/Themes' import { serverFeatures } from '../Lists/ServerFeatures' import { clientFeatures } from '../Lists/ClientFeatures' import { GetDeprecatedFeatures } from '../Lists/DeprecatedFeatures' @@ -9,14 +7,7 @@ import { experimentalFeatures } from '../Lists/ExperimentalFeatures' import { SubscriptionName } from '@standardnotes/common' export function GetFeatures(): FeatureDescription[] { - return [ - ...themes(), - ...editors(), - ...serverFeatures(), - ...clientFeatures(), - ...experimentalFeatures(), - ...GetDeprecatedFeatures(), - ] + return [...serverFeatures(), ...clientFeatures(), ...experimentalFeatures(), ...GetDeprecatedFeatures()] } export function GetFeaturesForSubscription(subscription: SubscriptionName): FeatureDescription[] { diff --git a/packages/features/src/Domain/Lists/ClientFeatures.ts b/packages/features/src/Domain/Lists/ClientFeatures.ts index 2a67eabd0..167166e11 100644 --- a/packages/features/src/Domain/Lists/ClientFeatures.ts +++ b/packages/features/src/Domain/Lists/ClientFeatures.ts @@ -1,14 +1,19 @@ -import { ClientFeatureDescription } from '../Feature/FeatureDescription' +import { FeatureDescription } from '../Feature/FeatureDescription' import { PermissionName } from '../Permission/PermissionName' import { FeatureIdentifier } from '../Feature/FeatureIdentifier' import { SubscriptionName } from '@standardnotes/common' import { RoleName } from '@standardnotes/domain-core' +import { themes } from './Themes' +import { editors } from './Editors' -export function clientFeatures(): ClientFeatureDescription[] { +export function clientFeatures(): FeatureDescription[] { return [ + ...themes(), + ...editors(), { - availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan], name: 'Tag Nesting', + availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan], + availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], identifier: FeatureIdentifier.TagNesting, permission_name: PermissionName.TagNesting, description: 'Organize your tags into folders.', @@ -17,45 +22,26 @@ export function clientFeatures(): ClientFeatureDescription[] { name: 'Super Notes', identifier: FeatureIdentifier.SuperEditor, availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan], + availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], permission_name: PermissionName.SuperEditor, description: - 'Type / to bring up the block selection menu, or @ to embed images or link other tags and notes. Type - then space to start a list, or [] then space to start a checklist. Drag and drop an image or file to embed it in your note.', - availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], + 'A new way to edit notes. Type / to bring up the block selection menu, or @ to embed images or link other tags and notes. Type - then space to start a list, or [] then space to start a checklist. Drag and drop an image or file to embed it in your note. Cmd/Ctrl + F to bring up search and replace.', }, { - availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan], name: 'Smart Filters', + availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan], + availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], identifier: FeatureIdentifier.SmartFilters, permission_name: PermissionName.SmartFilters, description: 'Create smart filters for viewing notes matching specific criteria.', }, { - availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan], name: 'Encrypted files', + availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan], + availableInRoles: [RoleName.NAMES.ProUser], identifier: FeatureIdentifier.Files, permission_name: PermissionName.Files, description: '', }, - { - availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan], - name: 'Focus Mode', - identifier: FeatureIdentifier.FocusMode, - permission_name: PermissionName.FocusMode, - description: '', - }, - { - availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan], - name: 'Listed Custom Domain', - identifier: FeatureIdentifier.ListedCustomDomain, - permission_name: PermissionName.ListedCustomDomain, - description: '', - }, - { - availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan], - name: 'Multiple accounts', - identifier: FeatureIdentifier.AccountSwitcher, - permission_name: PermissionName.AccountSwitcher, - description: '', - }, ] } diff --git a/packages/features/src/Domain/Lists/DeprecatedFeatures.ts b/packages/features/src/Domain/Lists/DeprecatedFeatures.ts index 4e2d9e4dd..306440aa2 100644 --- a/packages/features/src/Domain/Lists/DeprecatedFeatures.ts +++ b/packages/features/src/Domain/Lists/DeprecatedFeatures.ts @@ -10,6 +10,7 @@ import { NoteType } from '../Component/NoteType' import { FillEditorComponentDefaults } from './Utilities/FillEditorComponentDefaults' import { ComponentAction } from '../Component/ComponentAction' import { ComponentArea } from '../Component/ComponentArea' +import { RoleName } from '@standardnotes/domain-core' export function GetDeprecatedFeatures(): FeatureDescription[] { const bold: EditorFeatureDescription = FillEditorComponentDefaults({ @@ -37,6 +38,7 @@ export function GetDeprecatedFeatures(): FeatureDescription[] { permission_name: PermissionName.BoldEditor, description: 'A simple and peaceful rich editor that helps you write and think clearly.', thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/bold.jpg', + availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], }) const markdownBasic: EditorFeatureDescription = FillEditorComponentDefaults({ @@ -50,6 +52,7 @@ export function GetDeprecatedFeatures(): FeatureDescription[] { permission_name: PermissionName.MarkdownBasicEditor, description: 'A Markdown editor with dynamic split-pane preview.', thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/simple-markdown.jpg', + availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], }) const markdownMinimist: EditorFeatureDescription = FillEditorComponentDefaults({ @@ -64,6 +67,7 @@ export function GetDeprecatedFeatures(): FeatureDescription[] { deprecated: true, description: 'A minimal Markdown editor with live rendering and in-text search via Ctrl/Cmd + F', thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/min-markdown.jpg', + availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], }) const markdownMath: EditorFeatureDescription = FillEditorComponentDefaults({ @@ -78,6 +82,7 @@ export function GetDeprecatedFeatures(): FeatureDescription[] { index_path: 'index.html', description: 'A beautiful split-pane Markdown editor with synced-scroll, LaTeX support, and colorful syntax.', thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/fancy-markdown.jpg', + availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], }) const filesafe: IframeComponentFeatureDescription = FillEditorComponentDefaults({ @@ -104,6 +109,7 @@ export function GetDeprecatedFeatures(): FeatureDescription[] { description: 'Encrypted attachments for your notes using your Dropbox, Google Drive, or WebDAV server. Limited to 50MB per file.', thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/FileSafe-banner.png', + availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], }) return [bold, markdownBasic, markdownMinimist, markdownMath, filesafe] diff --git a/packages/features/src/Domain/Lists/Editors.ts b/packages/features/src/Domain/Lists/Editors.ts index f7446c848..8cc277f4a 100644 --- a/packages/features/src/Domain/Lists/Editors.ts +++ b/packages/features/src/Domain/Lists/Editors.ts @@ -4,6 +4,7 @@ import { PermissionName } from '../Permission/PermissionName' import { FeatureIdentifier } from '../Feature/FeatureIdentifier' import { NoteType } from '../Component/NoteType' import { FillEditorComponentDefaults } from './Utilities/FillEditorComponentDefaults' +import { RoleName } from '@standardnotes/domain-core' export function editors(): EditorFeatureDescription[] { const code: EditorFeatureDescription = FillEditorComponentDefaults({ @@ -20,6 +21,7 @@ export function editors(): EditorFeatureDescription[] { 'Syntax highlighting and convenient keyboard shortcuts for over 120 programming' + ' languages. Ideal for code snippets and procedures.', thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/code.jpg', + availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], }) const plus: EditorFeatureDescription = FillEditorComponentDefaults({ @@ -33,6 +35,7 @@ export function editors(): EditorFeatureDescription[] { description: 'From highlighting to custom font sizes and colors, to tables and lists, this editor is perfect for crafting any document.', thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/plus-editor.jpg', + availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], }) const markdown: EditorFeatureDescription = FillEditorComponentDefaults({ @@ -46,6 +49,7 @@ export function editors(): EditorFeatureDescription[] { description: 'A fully featured Markdown editor that supports live preview, a styling toolbar, and split pane support.', thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/adv-markdown.jpg', + availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], }) const markdownAlt: EditorFeatureDescription = FillEditorComponentDefaults({ @@ -59,6 +63,7 @@ export function editors(): EditorFeatureDescription[] { description: 'A WYSIWYG-style Markdown editor that renders Markdown in preview-mode while you type without displaying any syntax.', index_path: 'build/index.html', + availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], }) const task: EditorFeatureDescription = FillEditorComponentDefaults({ @@ -73,6 +78,7 @@ export function editors(): EditorFeatureDescription[] { description: 'A great way to manage short-term and long-term to-do"s. You can mark tasks as completed, change their order, and edit the text naturally in place.', thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/task-editor.jpg', + availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], }) const tokenvault: EditorFeatureDescription = FillEditorComponentDefaults({ @@ -86,6 +92,7 @@ export function editors(): EditorFeatureDescription[] { description: 'Encrypt and protect your 2FA secrets for all your internet accounts. Authenticator handles your 2FA secrets so that you never lose them again, or have to start over when you get a new device.', thumbnail_url: 'https://standard-notes.s3.amazonaws.com/screenshots/models/editors/token-vault.png', + availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], }) const spreadsheets: EditorFeatureDescription = FillEditorComponentDefaults({ @@ -99,6 +106,7 @@ export function editors(): EditorFeatureDescription[] { description: 'A powerful spreadsheet editor with formatting and formula support. Not recommended for large data sets, as encryption of such data may decrease editor performance.', thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/spreadsheets.png', + availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], }) return [code, plus, markdown, markdownAlt, task, tokenvault, spreadsheets] diff --git a/packages/features/src/Domain/Lists/ServerFeatures.ts b/packages/features/src/Domain/Lists/ServerFeatures.ts index 37c0389d4..9448215c2 100644 --- a/packages/features/src/Domain/Lists/ServerFeatures.ts +++ b/packages/features/src/Domain/Lists/ServerFeatures.ts @@ -65,5 +65,11 @@ export function serverFeatures(): ServerFeatureDescription[] { identifier: FeatureIdentifier.SubscriptionSharing, permission_name: PermissionName.SubscriptionSharing, }, + { + availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan], + name: 'Listed Custom Domain', + identifier: FeatureIdentifier.ListedCustomDomain, + permission_name: PermissionName.ListedCustomDomain, + }, ] } diff --git a/packages/features/src/Domain/Lists/Themes.ts b/packages/features/src/Domain/Lists/Themes.ts index e03e550ce..247e14f57 100644 --- a/packages/features/src/Domain/Lists/Themes.ts +++ b/packages/features/src/Domain/Lists/Themes.ts @@ -3,6 +3,7 @@ import { PermissionName } from '../Permission/PermissionName' import { FeatureIdentifier } from '../Feature/FeatureIdentifier' import { FillThemeComponentDefaults } from './Utilities/FillThemeComponentDefaults' import { SubscriptionName } from '@standardnotes/common' +import { RoleName } from '@standardnotes/domain-core' export function themes(): ThemeFeatureDescription[] { const midnight: ThemeFeatureDescription = FillThemeComponentDefaults({ @@ -17,10 +18,12 @@ export function themes(): ThemeFeatureDescription[] { foreground_color: '#ffffff', border_color: '#086DD6', }, + availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], }) const futura: ThemeFeatureDescription = FillThemeComponentDefaults({ availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan], + availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], name: 'Futura', identifier: FeatureIdentifier.FuturaTheme, permission_name: PermissionName.FuturaTheme, @@ -35,6 +38,7 @@ export function themes(): ThemeFeatureDescription[] { const solarizedDark: ThemeFeatureDescription = FillThemeComponentDefaults({ availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan], + availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], name: 'Solarized Dark', identifier: FeatureIdentifier.SolarizedDarkTheme, permission_name: PermissionName.SolarizedDarkTheme, @@ -49,6 +53,7 @@ export function themes(): ThemeFeatureDescription[] { const autobiography: ThemeFeatureDescription = FillThemeComponentDefaults({ availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan], + availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], name: 'Autobiography', identifier: FeatureIdentifier.AutobiographyTheme, permission_name: PermissionName.AutobiographyTheme, @@ -77,6 +82,7 @@ export function themes(): ThemeFeatureDescription[] { const titanium: ThemeFeatureDescription = FillThemeComponentDefaults({ availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan], + availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], name: 'Titanium', identifier: FeatureIdentifier.TitaniumTheme, permission_name: PermissionName.TitaniumTheme, @@ -90,6 +96,7 @@ export function themes(): ThemeFeatureDescription[] { const dynamic: ThemeFeatureDescription = FillThemeComponentDefaults({ availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan], + availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], name: 'Dynamic Panels', identifier: FeatureIdentifier.DynamicTheme, permission_name: PermissionName.ThemeDynamic, diff --git a/packages/responses/src/Domain/User/GetOfflineFeaturesResponse.ts b/packages/responses/src/Domain/User/GetOfflineFeaturesResponse.ts index ec5edb54a..4c564ab1f 100644 --- a/packages/responses/src/Domain/User/GetOfflineFeaturesResponse.ts +++ b/packages/responses/src/Domain/User/GetOfflineFeaturesResponse.ts @@ -4,5 +4,6 @@ import { MinimalHttpResponse } from '../Http/MinimalHttpResponses' export type GetOfflineFeaturesResponse = MinimalHttpResponse & { data?: { features: FeatureDescription[] + roles: string[] } } diff --git a/packages/services/src/Domain/Feature/FeaturesClientInterface.ts b/packages/services/src/Domain/Feature/FeaturesClientInterface.ts index 21db88300..7d8862852 100644 --- a/packages/services/src/Domain/Feature/FeaturesClientInterface.ts +++ b/packages/services/src/Domain/Feature/FeaturesClientInterface.ts @@ -1,4 +1,4 @@ -import { FeatureDescription, FeatureIdentifier } from '@standardnotes/features' +import { FeatureIdentifier } from '@standardnotes/features' import { SNComponent } from '@standardnotes/models' import { FeatureStatus } from './FeatureStatus' @@ -7,10 +7,10 @@ import { SetOfflineFeaturesFunctionResponse } from './SetOfflineFeaturesFunction export interface FeaturesClientInterface { downloadExternalFeature(urlOrCode: string): Promise - getUserFeature(featureId: FeatureIdentifier): FeatureDescription | undefined - getFeatureStatus(featureId: FeatureIdentifier): FeatureStatus + hasFirstPartySubscription(): boolean + hasMinimumRole(role: string): boolean setOfflineFeaturesCode(code: string): Promise diff --git a/packages/services/src/Domain/Storage/StorageKeys.ts b/packages/services/src/Domain/Storage/StorageKeys.ts index d22fc232e..55ce4ba6b 100644 --- a/packages/services/src/Domain/Storage/StorageKeys.ts +++ b/packages/services/src/Domain/Storage/StorageKeys.ts @@ -34,6 +34,7 @@ export enum StorageKey { StorageEncryptionPolicy = 'storage_policy', WebSocketUrl = 'webSocket_url', UserRoles = 'user_roles', + OfflineUserRoles = 'offline_user_roles', UserFeatures = 'user_features', ExperimentalFeatures = 'experimental_features', DeinitMode = 'deinit_mode', diff --git a/packages/snjs/lib/Application/Application.ts b/packages/snjs/lib/Application/Application.ts index 653f8667e..77e555106 100644 --- a/packages/snjs/lib/Application/Application.ts +++ b/packages/snjs/lib/Application/Application.ts @@ -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() { diff --git a/packages/snjs/lib/Hosts.ts b/packages/snjs/lib/Hosts.ts index 1ba17f90e..9f88db3a3 100644 --- a/packages/snjs/lib/Hosts.ts +++ b/packages/snjs/lib/Hosts.ts @@ -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', } diff --git a/packages/snjs/lib/Services/Api/ApiService.ts b/packages/snjs/lib/Services/Api/ApiService.ts index 60b60e9e0..bf22149cb 100644 --- a/packages/snjs/lib/Services/Api/ApiService.ts +++ b/packages/snjs/lib/Services/Api/ApiService.ts @@ -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) diff --git a/packages/snjs/lib/Services/Api/WebsocketsService.ts b/packages/snjs/lib/Services/Api/WebsocketsService.ts index 3a8fa7788..fc76572fd 100644 --- a/packages/snjs/lib/Services/Api/WebsocketsService.ts +++ b/packages/snjs/lib/Services/Api/WebsocketsService.ts @@ -80,7 +80,7 @@ export class SNWebSocketsService extends AbstractService { 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 + + 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 - - 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) diff --git a/packages/snjs/lib/Services/Features/FeaturesService.ts b/packages/snjs/lib/Services/Features/FeaturesService.ts index dc57038bf..db1718271 100644 --- a/packages/snjs/lib/Services/Features/FeaturesService.ts +++ b/packages/snjs/lib/Services/Features/FeaturesService.ts @@ -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(StorageKey.UserRoles, undefined, []) + + this.offlineRoles = this.storageService.getValue(StorageKey.OfflineUserRoles, undefined, []) + + this.features = this.storageService.getValue(StorageKey.UserFeatures, undefined, []) + + this.enabledExperimentalFeatures = this.storageService.getValue(StorageKey.ExperimentalFeatures, undefined, []) + } + async handleEvent(event: InternalEventInterface): Promise { 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 { 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(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 { - 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 { + 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 { - const rolesChanged = !arraysEqual(this.roles, roles) + async setOnlineRoles(roles: string[]): Promise { + 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 { + 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 { @@ -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 { return Promise.resolve({ features: { - roles: this.roles, + roles: this.onlineRoles, features: this.features, enabledExperimentalFeatures: this.enabledExperimentalFeatures, needsInitialFeaturesUpdate: this.needsInitialFeaturesUpdate, diff --git a/packages/snjs/lib/Services/Settings/SNSettingsService.ts b/packages/snjs/lib/Services/Settings/SNSettingsService.ts index cc4621be7..89f7632e0 100644 --- a/packages/snjs/lib/Services/Settings/SNSettingsService.ts +++ b/packages/snjs/lib/Services/Settings/SNSettingsService.ts @@ -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 { diff --git a/packages/snjs/mocha/features.test.js b/packages/snjs/mocha/features.test.js index 7b624dc5f..31419d60d 100644 --- a/packages/snjs/mocha/features.test.js +++ b/packages/snjs/mocha/features.test.js @@ -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) }) diff --git a/packages/snjs/mocha/keys.test.js b/packages/snjs/mocha/keys.test.js index 222f483ff..bbdd4d46b 100644 --- a/packages/snjs/mocha/keys.test.js +++ b/packages/snjs/mocha/keys.test.js @@ -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 () { diff --git a/packages/snjs/mocha/sync_tests/conflicting.test.js b/packages/snjs/mocha/sync_tests/conflicting.test.js index 07d3be138..1c5c3817a 100644 --- a/packages/snjs/mocha/sync_tests/conflicting.test.js +++ b/packages/snjs/mocha/sync_tests/conflicting.test.js @@ -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) }) diff --git a/packages/snjs/mocha/sync_tests/online.test.js b/packages/snjs/mocha/sync_tests/online.test.js index ffbbda1cd..1ab803218 100644 --- a/packages/snjs/mocha/sync_tests/online.test.js +++ b/packages/snjs/mocha/sync_tests/online.test.js @@ -1052,5 +1052,7 @@ describe('online syncing', function () { await contextB.sync() expect(contextB.application.items.allCountableNotesCount()).to.equal(0) + + await contextB.deinit() }) }) diff --git a/packages/web/CHANGELOG.md b/packages/web/CHANGELOG.md index 86e77d287..fb8488483 100644 --- a/packages/web/CHANGELOG.md +++ b/packages/web/CHANGELOG.md @@ -28,7 +28,6 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Features * Added rename option to file preview modal ([aa88966](https://github.com/standardnotes/app/commit/aa8896678315de67551c786e64aec7dfd10479e3)) -* **snjs:** add revisions api v2 ([#2154](https://github.com/standardnotes/app/issues/2154)) ([880a537](https://github.com/standardnotes/app/commit/880a537774ddcefaedb0d4e5dc50b363f4b93e01)) ## [3.138.6](https://github.com/standardnotes/app/compare/@standardnotes/web@3.138.5...@standardnotes/web@3.138.6) (2023-01-17) diff --git a/packages/web/CHANGELOG.md.json b/packages/web/CHANGELOG.md.json index 9c834e579..238b7c54f 100644 --- a/packages/web/CHANGELOG.md.json +++ b/packages/web/CHANGELOG.md.json @@ -50,12 +50,10 @@ "body": "### Features\n\n* Added rename option to file preview modal ([aa88966](https://github.com/standardnotes/app/commit/aa8896678315de67551c786e64aec7dfd10479e3))\n* **snjs:** add revisions api v2 ([#2154](https://github.com/standardnotes/app/issues/2154)) ([880a537](https://github.com/standardnotes/app/commit/880a537774ddcefaedb0d4e5dc50b363f4b93e01))", "parsed": { "_": [ - "Added rename option to file preview modal (aa88966)", - "snjs: add revisions api v2 (#2154) (880a537)" + "Added rename option to file preview modal (aa88966)" ], "Features": [ - "Added rename option to file preview modal (aa88966)", - "snjs: add revisions api v2 (#2154) (880a537)" + "Added rename option to file preview modal (aa88966)" ] } }, diff --git a/packages/web/src/javascripts/Components/Footer/UpgradeNow.tsx b/packages/web/src/javascripts/Components/Footer/UpgradeNow.tsx index edd18750a..719b08a99 100644 --- a/packages/web/src/javascripts/Components/Footer/UpgradeNow.tsx +++ b/packages/web/src/javascripts/Components/Footer/UpgradeNow.tsx @@ -13,6 +13,7 @@ type Props = { const UpgradeNow = ({ application, featuresController, subscriptionContoller }: Props) => { const shouldShowCTA = !featuresController.hasFolders const hasAccount = subscriptionContoller.hasAccount + const hasAccessToFeatures = subscriptionContoller.hasFirstPartySubscription const onClick = useCallback(() => { if (hasAccount && application.isNativeIOS()) { @@ -22,16 +23,20 @@ const UpgradeNow = ({ application, featuresController, subscriptionContoller }: } }, [application, hasAccount]) - return shouldShowCTA ? ( + if (!shouldShowCTA || hasAccessToFeatures) { + return null + } + + return (
- ) : null + ) } export default observer(UpgradeNow) diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Account/Email.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Account/Email.tsx index deaba555b..fa6306631 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Account/Email.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Account/Email.tsx @@ -99,7 +99,7 @@ const Email: FunctionComponent = ({ application }: Props) => { Disable sign-in notification emails Disables email notifications when a new sign-in occurs on your account. (Email notifications are - available to paid subscribers). + available only to paid subscribers). {isLoading ? ( diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Account/Subscription/Subscription.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Account/Subscription/Subscription.tsx index 6d7ffdf67..f8b111167 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Account/Subscription/Subscription.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Account/Subscription/Subscription.tsx @@ -15,7 +15,7 @@ type Props = { const Subscription: FunctionComponent = ({ application, viewControllerManager }: Props) => { const subscriptionState = viewControllerManager.subscriptionController - const { userSubscription } = subscriptionState + const { onlineSubscription } = subscriptionState const now = new Date().getTime() @@ -25,7 +25,7 @@ const Subscription: FunctionComponent = ({ application, viewControllerMan
Subscription - {userSubscription && userSubscription.endsAt > now ? ( + {onlineSubscription && onlineSubscription.endsAt > now ? ( ) : ( diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Backups/CloudBackups/CloudBackupProvider.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Backups/CloudBackups/CloudBackupProvider.tsx index 6d7eec50c..cbb651d68 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Backups/CloudBackups/CloudBackupProvider.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Backups/CloudBackups/CloudBackupProvider.tsx @@ -17,7 +17,7 @@ import { } from '@standardnotes/snjs' import { WebApplication } from '@/Application/Application' import Button from '@/Components/Button/Button' -import { isDev, openInNewTab } from '@/Utils' +import { openInNewTab } from '@/Utils' import { Subtitle } from '@/Components/Preferences/PreferencesComponents/Content' import { KeyboardKey } from '@standardnotes/ui-services' @@ -61,7 +61,7 @@ const CloudBackupProvider: FunctionComponent = ({ application, providerNa } event.stopPropagation() - const authUrl = application.getCloudProviderIntegrationUrl(providerName, isDev) + const authUrl = application.getCloudProviderIntegrationUrl(providerName) openInNewTab(authUrl) setAuthBegan(true) } diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/General/Advanced/Packages/Provider/PackageProvider.ts b/packages/web/src/javascripts/Components/Preferences/Panes/General/Advanced/Packages/Provider/PackageProvider.ts index 27cc2511c..c2ab019e6 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/General/Advanced/Packages/Provider/PackageProvider.ts +++ b/packages/web/src/javascripts/Components/Preferences/Panes/General/Advanced/Packages/Provider/PackageProvider.ts @@ -7,7 +7,7 @@ export class PackageProvider { static async load(application: WebApplication): Promise { const response = await application.getAvailableSubscriptions() - if (response instanceof ClientDisplayableError) { + if (!response || response instanceof ClientDisplayableError) { return undefined } diff --git a/packages/web/src/javascripts/Components/PremiumFeaturesModal/Subviews/UpgradePrompt.tsx b/packages/web/src/javascripts/Components/PremiumFeaturesModal/Subviews/UpgradePrompt.tsx index 3d75ad7b1..95c03ba9a 100644 --- a/packages/web/src/javascripts/Components/PremiumFeaturesModal/Subviews/UpgradePrompt.tsx +++ b/packages/web/src/javascripts/Components/PremiumFeaturesModal/Subviews/UpgradePrompt.tsx @@ -64,7 +64,10 @@ export const UpgradePrompt = ({
The Professional Plan costs $119.99/year and includes benefits like
  • 100GB encrypted file storage
  • -
  • Access to all note types, including markdown, rich text, authenticator, tasks, and spreadsheets
  • +
  • + Access to all note types, including Super, markdown, rich text, authenticator, tasks, and spreadsheets +
  • +
  • Access to Daily Notebooks and Moments journals
  • Note history going back indefinitely
  • Nested folders for your tags
  • Premium support
  • @@ -79,7 +82,7 @@ export const UpgradePrompt = ({ className="no-border w-full cursor-pointer rounded bg-info py-2 font-bold text-info-contrast hover:brightness-125 focus:brightness-125" ref={ctaRef} > - Upgrade + {application.isNativeIOS() ? 'Start Free Trial' : 'Upgrade'}
diff --git a/packages/web/src/javascripts/Controllers/FeaturesController.ts b/packages/web/src/javascripts/Controllers/FeaturesController.ts index bc2d40de7..b9bedbace 100644 --- a/packages/web/src/javascripts/Controllers/FeaturesController.ts +++ b/packages/web/src/javascripts/Controllers/FeaturesController.ts @@ -65,6 +65,7 @@ export class FeaturesController extends AbstractViewController { break case ApplicationEvent.FeaturesUpdated: case ApplicationEvent.Launched: + case ApplicationEvent.LocalDataLoaded: runInAction(() => { this.hasFolders = this.isEntitledToFolders() this.hasSmartViews = this.isEntitledToSmartViews() diff --git a/packages/web/src/javascripts/Controllers/LinkingController.tsx b/packages/web/src/javascripts/Controllers/LinkingController.tsx index 990052764..43b43c933 100644 --- a/packages/web/src/javascripts/Controllers/LinkingController.tsx +++ b/packages/web/src/javascripts/Controllers/LinkingController.tsx @@ -75,7 +75,7 @@ export class LinkingController extends AbstractViewController { } get isEntitledToNoteLinking() { - return !!this.subscriptionController.userSubscription + return !!this.subscriptionController.onlineSubscription } setIsLinkingPanelOpen = (open: boolean) => { diff --git a/packages/web/src/javascripts/Controllers/Subscription/SubscriptionController.ts b/packages/web/src/javascripts/Controllers/Subscription/SubscriptionController.ts index 4c26f5fe1..bc60bb1ae 100644 --- a/packages/web/src/javascripts/Controllers/Subscription/SubscriptionController.ts +++ b/packages/web/src/javascripts/Controllers/Subscription/SubscriptionController.ts @@ -17,14 +17,15 @@ import { Subscription } from './SubscriptionType' export class SubscriptionController extends AbstractViewController { private readonly ALLOWED_SUBSCRIPTION_INVITATIONS = 5 - userSubscription: Subscription | undefined = undefined + onlineSubscription: Subscription | undefined = undefined availableSubscriptions: AvailableSubscriptions | undefined = undefined subscriptionInvitations: Invitation[] | undefined = undefined hasAccount: boolean + hasFirstPartySubscription: boolean override deinit() { super.deinit() - ;(this.userSubscription as unknown) = undefined + ;(this.onlineSubscription as unknown) = undefined ;(this.availableSubscriptions as unknown) = undefined ;(this.subscriptionInvitations as unknown) = undefined @@ -38,12 +39,14 @@ export class SubscriptionController extends AbstractViewController { ) { super(application, eventBus) this.hasAccount = application.hasAccount() + this.hasFirstPartySubscription = application.features.hasFirstPartySubscription() makeObservable(this, { - userSubscription: observable, + onlineSubscription: observable, availableSubscriptions: observable, subscriptionInvitations: observable, hasAccount: observable, + hasFirstPartySubscription: observable, userSubscriptionName: computed, userSubscriptionExpirationDate: computed, @@ -64,11 +67,20 @@ export class SubscriptionController extends AbstractViewController { this.reloadSubscriptionInvitations().catch(console.error) } runInAction(() => { + this.hasFirstPartySubscription = application.features.hasFirstPartySubscription() this.hasAccount = application.hasAccount() }) }, ApplicationEvent.Launched), ) + this.disposers.push( + application.addEventObserver(async () => { + runInAction(() => { + this.hasFirstPartySubscription = application.features.hasFirstPartySubscription() + }) + }, ApplicationEvent.LocalDataLoaded), + ) + this.disposers.push( application.addEventObserver(async () => { this.getSubscriptionInfo().catch(console.error) @@ -83,6 +95,9 @@ export class SubscriptionController extends AbstractViewController { application.addEventObserver(async () => { this.getSubscriptionInfo().catch(console.error) this.reloadSubscriptionInvitations().catch(console.error) + runInAction(() => { + this.hasFirstPartySubscription = application.features.hasFirstPartySubscription() + }) }, ApplicationEvent.UserRolesChanged), ) } @@ -90,20 +105,20 @@ export class SubscriptionController extends AbstractViewController { get userSubscriptionName(): string { if ( this.availableSubscriptions && - this.userSubscription && - this.availableSubscriptions[this.userSubscription.planName] + this.onlineSubscription && + this.availableSubscriptions[this.onlineSubscription.planName] ) { - return this.availableSubscriptions[this.userSubscription.planName].name + return this.availableSubscriptions[this.onlineSubscription.planName].name } return '' } get userSubscriptionExpirationDate(): Date | undefined { - if (!this.userSubscription) { + if (!this.onlineSubscription) { return undefined } - return new Date(convertTimestampToMilliseconds(this.userSubscription.endsAt)) + return new Date(convertTimestampToMilliseconds(this.onlineSubscription.endsAt)) } get isUserSubscriptionExpired(): boolean { @@ -115,11 +130,11 @@ export class SubscriptionController extends AbstractViewController { } get isUserSubscriptionCanceled(): boolean { - return Boolean(this.userSubscription?.cancelled) + return Boolean(this.onlineSubscription?.cancelled) } hasValidSubscription(): boolean { - return this.userSubscription != undefined && !this.isUserSubscriptionExpired && !this.isUserSubscriptionCanceled + return this.onlineSubscription != undefined && !this.isUserSubscriptionExpired && !this.isUserSubscriptionCanceled } get usedInvitationsCount(): number { @@ -139,7 +154,7 @@ export class SubscriptionController extends AbstractViewController { } public setUserSubscription(subscription: Subscription): void { - this.userSubscription = subscription + this.onlineSubscription = subscription } public setAvailableSubscriptions(subscriptions: AvailableSubscriptions): void { diff --git a/packages/web/src/javascripts/Utils/createEditorMenuGroups.ts b/packages/web/src/javascripts/Utils/createEditorMenuGroups.ts index 8c5ca46b7..c0152ccf5 100644 --- a/packages/web/src/javascripts/Utils/createEditorMenuGroups.ts +++ b/packages/web/src/javascripts/Utils/createEditorMenuGroups.ts @@ -6,6 +6,7 @@ import { ComponentArea, FeatureDescription, GetFeatures, + FindNativeFeature, NoteType, FeatureIdentifier, } from '@standardnotes/snjs' @@ -149,8 +150,7 @@ const createBaselineMap = (application: WebApplication): NoteTypeToEditorRowsMap isEntitled: application.features.getFeatureStatus(FeatureIdentifier.SuperEditor) === FeatureStatus.Entitled, noteType: NoteType.Super, isLabs: true, - description: - 'A new way to edit notes. Type / to bring up the block selection menu, or @ to embed images or link other tags and notes. Type - then space to start a list, or [] then space to start a checklist. Drag and drop an image or file to embed it in your note.', + description: FindNativeFeature(FeatureIdentifier.SuperEditor)?.description, }, ], [NoteType.RichText]: [],