From d268c02ab31beb5e2fd9e6547610f9a4dd61bed4 Mon Sep 17 00:00:00 2001 From: Mo Date: Wed, 26 Jul 2023 15:50:08 -0500 Subject: [PATCH] fix: Fixes issue where lock screen would not use previously active theme (#2372) --- .../src/Domain/Component/EditorIdentifier.ts | 5 - .../src/Domain/Component/NoteType.spec.ts | 18 +- .../features/src/Domain/Component/NoteType.ts | 8 +- .../Domain/Feature/BaseFeatureDescription.ts | 3 +- .../Feature/ClientFeatureDescription.ts | 3 +- .../src/Domain/Feature/FeatureIdentifier.ts | 51 --- .../features/src/Domain/Feature/Features.ts | 12 +- .../Domain/Feature/NativeFeatureIdentifier.ts | 75 +++++ .../Feature/ServerFeatureDescription.ts | 3 +- .../src/Domain/Lists/ClientFeatures.ts | 14 +- .../src/Domain/Lists/DeprecatedFeatures.ts | 14 +- .../src/Domain/Lists/IframeEditors.ts | 14 +- .../src/Domain/Lists/NativeEditors.ts | 6 +- .../src/Domain/Lists/ServerFeatures.ts | 22 +- packages/features/src/Domain/Lists/Themes.ts | 16 +- .../src/Domain/Permission/PermissionName.ts | 2 +- packages/features/src/Domain/index.ts | 3 +- .../Domain/Runtime/Feature/TypeGuards.spec.ts | 16 +- .../src/Domain/Runtime/Feature/TypeGuards.ts | 8 +- .../src/Domain/Runtime/Feature/UIFeature.ts | 61 ++-- .../Runtime/Feature/UIFeatureInterface.ts | 7 +- .../Domain/Syncable/Component/Component.ts | 3 +- .../Syncable/Component/ComponentInterface.ts | 10 +- .../models/src/Domain/Syncable/Note/Note.ts | 4 +- .../src/Domain/Syncable/Note/NoteContent.ts | 4 +- .../Domain/Syncable/Note/NoteMutator.spec.ts | 6 +- .../src/Domain/Syncable/Note/NoteMutator.ts | 4 +- .../src/Domain/Syncable/Tag/TagPreferences.ts | 3 +- .../UserPrefs/ComponentPreferences.ts | 6 +- .../Domain/Syncable/UserPrefs/PrefDefaults.ts | 6 +- .../src/Domain/Syncable/UserPrefs/PrefKey.ts | 7 +- .../Component/ComponentManagerInterface.ts | 7 +- .../Component/ComponentViewerInterface.ts | 5 +- .../Domain/Feature/FeaturesClientInterface.ts | 23 +- .../src/Domain/Session/SessionEvent.ts | 6 +- .../Domain/Session/SessionsClientInterface.ts | 2 - .../Subscription/SubscriptionManager.spec.ts | 58 ++++ .../Subscription/SubscriptionManager.ts | 12 +- packages/snjs/lib/Application/Application.ts | 6 +- .../snjs/lib/Migrations/Versions/2_42_0.ts | 3 +- .../ComponentManager/ComponentManager.ts | 66 ++-- .../ComponentManager/ComponentViewer.ts | 41 +-- .../lib/Services/ComponentManager/Types.ts | 8 +- .../DoesEditorChangeRequireAlert.spec.ts | 48 ++- .../UseCase/EditorForNote.spec.ts | 4 +- .../ComponentManager/UseCase/EditorForNote.ts | 7 +- .../GetDefaultEditorIdentifier.spec.ts | 16 +- .../UseCase/GetDefaultEditorIdentifier.ts | 8 +- .../UseCase/GetFeatureUrl.spec.ts | 16 +- .../UseCase/RunWithPermissionsUseCase.spec.ts | 23 +- .../UseCase/RunWithPermissionsUseCase.ts | 3 +- .../Services/Features/FeaturesService.spec.ts | 294 +++++++++++------- .../lib/Services/Features/FeaturesService.ts | 74 +++-- .../Features/UseCase/GetFeatureStatus.spec.ts | 61 ++-- .../Features/UseCase/GetFeatureStatus.ts | 26 +- .../lib/Services/Session/SessionManager.ts | 18 +- .../snjs/mocha/migrations/migration.test.js | 4 +- .../snjs/mocha/model_tests/appmodels.test.js | 4 +- packages/ui-services/package.json | 2 +- .../AegisToAuthenticatorConverter.spec.ts | 4 +- .../AegisToAuthenticatorConverter.ts | 4 +- packages/ui-services/src/Import/Importer.ts | 6 +- .../src/Theme/ActiveThemeList.spec.ts | 58 ++++ .../ui-services/src/Theme/ActiveThemeList.ts | 70 +++++ .../ui-services/src/Theme/ThemeManager.ts | 147 +++++---- packages/utils/package.json | 2 +- packages/utils/src/Domain/Utils/Utils.spec.ts | 37 ++- packages/utils/src/Domain/Utils/Utils.ts | 4 + .../ChangeEditor/ChangeEditorMenu.tsx | 6 +- .../ChangeEditor/ChangeEditorMultipleMenu.tsx | 2 +- .../Components/ClipperView/ClipperView.tsx | 21 +- .../ComponentView/IframeFeatureView.tsx | 2 +- .../Header/NewNotePreferences.tsx | 27 +- .../Controller/NoteViewController.spec.ts | 15 +- .../Components/NoteView/NoteView.tsx | 55 ++-- .../Preferences/Panes/Account/Email/Email.tsx | 6 +- .../SubscriptionSharing.tsx | 7 +- .../Preferences/Panes/Appearance.tsx | 16 +- .../Preferences/Panes/General/Labs/Labs.tsx | 8 +- .../Preferences/Panes/Security/Security.tsx | 7 +- .../QuickSettingsMenu/QuickSettingsMenu.tsx | 8 +- .../QuickSettingsMenu/ThemesMenuButton.tsx | 10 +- .../CompoundPredicateBuilderController.ts | 4 +- .../Components/SuperEditor/SuperEditor.tsx | 9 +- .../Controllers/FeaturesController.ts | 14 +- .../Utils/DropdownItemsForEditors.ts | 14 +- .../web/src/javascripts/Utils/SortThemes.ts | 4 +- .../Utils/createEditorMenuGroups.ts | 8 +- 88 files changed, 1118 insertions(+), 716 deletions(-) delete mode 100644 packages/features/src/Domain/Component/EditorIdentifier.ts delete mode 100644 packages/features/src/Domain/Feature/FeatureIdentifier.ts create mode 100644 packages/features/src/Domain/Feature/NativeFeatureIdentifier.ts create mode 100644 packages/ui-services/src/Theme/ActiveThemeList.spec.ts create mode 100644 packages/ui-services/src/Theme/ActiveThemeList.ts diff --git a/packages/features/src/Domain/Component/EditorIdentifier.ts b/packages/features/src/Domain/Component/EditorIdentifier.ts deleted file mode 100644 index 82de7f959..000000000 --- a/packages/features/src/Domain/Component/EditorIdentifier.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { FeatureIdentifier } from './../Feature/FeatureIdentifier' - -type ThirdPartyIdentifier = string - -export type EditorIdentifier = FeatureIdentifier | ThirdPartyIdentifier diff --git a/packages/features/src/Domain/Component/NoteType.spec.ts b/packages/features/src/Domain/Component/NoteType.spec.ts index 7fcc6e1ff..6879d5543 100644 --- a/packages/features/src/Domain/Component/NoteType.spec.ts +++ b/packages/features/src/Domain/Component/NoteType.spec.ts @@ -1,16 +1,16 @@ -import { FeatureIdentifier } from '@standardnotes/features' +import { NativeFeatureIdentifier } from '@standardnotes/features' import { noteTypeForEditorIdentifier, NoteType } from './NoteType' describe('note type', () => { it('should return the correct note type for editor identifier', () => { - expect(noteTypeForEditorIdentifier(FeatureIdentifier.PlainEditor)).toEqual(NoteType.Plain) - expect(noteTypeForEditorIdentifier(FeatureIdentifier.SuperEditor)).toEqual(NoteType.Super) - expect(noteTypeForEditorIdentifier(FeatureIdentifier.MarkdownProEditor)).toEqual(NoteType.Markdown) - expect(noteTypeForEditorIdentifier(FeatureIdentifier.PlusEditor)).toEqual(NoteType.RichText) - expect(noteTypeForEditorIdentifier(FeatureIdentifier.CodeEditor)).toEqual(NoteType.Code) - expect(noteTypeForEditorIdentifier(FeatureIdentifier.SheetsEditor)).toEqual(NoteType.Spreadsheet) - expect(noteTypeForEditorIdentifier(FeatureIdentifier.TaskEditor)).toEqual(NoteType.Task) - expect(noteTypeForEditorIdentifier(FeatureIdentifier.TokenVaultEditor)).toEqual(NoteType.Authentication) + expect(noteTypeForEditorIdentifier(NativeFeatureIdentifier.TYPES.PlainEditor)).toEqual(NoteType.Plain) + expect(noteTypeForEditorIdentifier(NativeFeatureIdentifier.TYPES.SuperEditor)).toEqual(NoteType.Super) + expect(noteTypeForEditorIdentifier(NativeFeatureIdentifier.TYPES.MarkdownProEditor)).toEqual(NoteType.Markdown) + expect(noteTypeForEditorIdentifier(NativeFeatureIdentifier.TYPES.PlusEditor)).toEqual(NoteType.RichText) + expect(noteTypeForEditorIdentifier(NativeFeatureIdentifier.TYPES.CodeEditor)).toEqual(NoteType.Code) + expect(noteTypeForEditorIdentifier(NativeFeatureIdentifier.TYPES.SheetsEditor)).toEqual(NoteType.Spreadsheet) + expect(noteTypeForEditorIdentifier(NativeFeatureIdentifier.TYPES.TaskEditor)).toEqual(NoteType.Task) + expect(noteTypeForEditorIdentifier(NativeFeatureIdentifier.TYPES.TokenVaultEditor)).toEqual(NoteType.Authentication) expect(noteTypeForEditorIdentifier('org.third.party')).toEqual(NoteType.Unknown) }) }) diff --git a/packages/features/src/Domain/Component/NoteType.ts b/packages/features/src/Domain/Component/NoteType.ts index 24d8355b3..9d8990407 100644 --- a/packages/features/src/Domain/Component/NoteType.ts +++ b/packages/features/src/Domain/Component/NoteType.ts @@ -1,8 +1,6 @@ import { EditorFeatureDescription } from '../Feature/EditorFeatureDescription' import { FindNativeFeature } from '../Feature/Features' import { IframeComponentFeatureDescription } from '../Feature/IframeComponentFeatureDescription' -import { FeatureIdentifier } from './../Feature/FeatureIdentifier' -import { EditorIdentifier } from './EditorIdentifier' export enum NoteType { Authentication = 'authentication', @@ -16,10 +14,8 @@ export enum NoteType { Unknown = 'unknown', } -export function noteTypeForEditorIdentifier(identifier: EditorIdentifier): NoteType { - const feature = FindNativeFeature( - identifier as FeatureIdentifier, - ) +export function noteTypeForEditorIdentifier(identifier: string): NoteType { + const feature = FindNativeFeature(identifier) if (feature && feature.note_type) { return feature.note_type } diff --git a/packages/features/src/Domain/Feature/BaseFeatureDescription.ts b/packages/features/src/Domain/Feature/BaseFeatureDescription.ts index df8bd98f4..420ae0b45 100644 --- a/packages/features/src/Domain/Feature/BaseFeatureDescription.ts +++ b/packages/features/src/Domain/Feature/BaseFeatureDescription.ts @@ -1,5 +1,4 @@ import { PermissionName } from '../Permission/PermissionName' -import { FeatureIdentifier } from './FeatureIdentifier' import { ComponentFlag } from '../Component/ComponentFlag' import { RoleFields } from './RoleFields' @@ -14,7 +13,7 @@ export type BaseFeatureDescription = RoleFields & { clientControlled?: boolean flags?: ComponentFlag[] - identifier: FeatureIdentifier + identifier: string marketing_url?: string name: string no_expire?: boolean diff --git a/packages/features/src/Domain/Feature/ClientFeatureDescription.ts b/packages/features/src/Domain/Feature/ClientFeatureDescription.ts index ccd0acfe5..ddd57e217 100644 --- a/packages/features/src/Domain/Feature/ClientFeatureDescription.ts +++ b/packages/features/src/Domain/Feature/ClientFeatureDescription.ts @@ -1,9 +1,8 @@ import { PermissionName } from '../Permission/PermissionName' -import { FeatureIdentifier } from './FeatureIdentifier' import { RoleFields } from './RoleFields' export type ClientFeatureDescription = RoleFields & { - identifier: FeatureIdentifier + identifier: string permission_name: PermissionName description: string name: string diff --git a/packages/features/src/Domain/Feature/FeatureIdentifier.ts b/packages/features/src/Domain/Feature/FeatureIdentifier.ts deleted file mode 100644 index c95090a7e..000000000 --- a/packages/features/src/Domain/Feature/FeatureIdentifier.ts +++ /dev/null @@ -1,51 +0,0 @@ -export enum FeatureIdentifier { - DailyEmailBackup = 'org.standardnotes.daily-email-backup', - Files = 'org.standardnotes.files', - FilesLowStorageTier = 'org.standardnotes.files-low-storage-tier', - FilesMaximumStorageTier = 'org.standardnotes.files-max-storage-tier', - ListedCustomDomain = 'org.standardnotes.listed-custom-domain', - NoteHistory30Days = 'org.standardnotes.note-history-30', - NoteHistory365Days = 'org.standardnotes.note-history-365', - NoteHistoryUnlimited = 'org.standardnotes.note-history-unlimited', - SignInAlerts = 'com.standardnotes.sign-in-alerts', - SmartFilters = 'org.standardnotes.smart-filters', - TagNesting = 'org.standardnotes.tag-nesting', - TwoFactorAuth = 'org.standardnotes.two-factor-auth', - UniversalSecondFactor = 'org.standardnotes.universal-second-factor', - SubscriptionSharing = 'org.standardnotes.subscription-sharing', - - AutobiographyTheme = 'org.standardnotes.theme-autobiography', - DynamicTheme = 'org.standardnotes.theme-dynamic', - DarkTheme = 'org.standardnotes.theme-focus', - FuturaTheme = 'org.standardnotes.theme-futura', - MidnightTheme = 'org.standardnotes.theme-midnight', - SolarizedDarkTheme = 'org.standardnotes.theme-solarized-dark', - TitaniumTheme = 'org.standardnotes.theme-titanium', - - PlainEditor = 'com.standardnotes.plain-text', - SuperEditor = 'com.standardnotes.super-editor', - - CodeEditor = 'org.standardnotes.code-editor', - MarkdownProEditor = 'org.standardnotes.advanced-markdown-editor', - PlusEditor = 'org.standardnotes.plus-editor', - SheetsEditor = 'org.standardnotes.standard-sheets', - TaskEditor = 'org.standardnotes.simple-task-editor', - TokenVaultEditor = 'org.standardnotes.token-vault', - - Extension = 'org.standardnotes.extension', - - DeprecatedMarkdownVisualEditor = 'org.standardnotes.markdown-visual-editor', - DeprecatedBoldEditor = 'org.standardnotes.bold-editor', - DeprecatedMarkdownBasicEditor = 'org.standardnotes.simple-markdown-editor', - DeprecatedMarkdownMathEditor = 'org.standardnotes.fancy-markdown-editor', - DeprecatedMarkdownMinimistEditor = 'org.standardnotes.minimal-markdown-editor', - DeprecatedFoldersComponent = 'org.standardnotes.folders', - DeprecatedFileSafe = 'org.standardnotes.file-safe', -} - -/** - * Identifier for standalone filesafe instance offered as legacy installable via extensions-server - */ -export const LegacyFileSafeIdentifier = 'org.standardnotes.legacy.file-safe' - -export const ExperimentalFeatures = [] diff --git a/packages/features/src/Domain/Feature/Features.ts b/packages/features/src/Domain/Feature/Features.ts index 7a7dec6df..cc194cb9a 100644 --- a/packages/features/src/Domain/Feature/Features.ts +++ b/packages/features/src/Domain/Feature/Features.ts @@ -1,7 +1,7 @@ import { AnyFeatureDescription } from './AnyFeatureDescription' import { ThemeFeatureDescription } from './ThemeFeatureDescription' import { EditorFeatureDescription } from './EditorFeatureDescription' -import { FeatureIdentifier } from './FeatureIdentifier' +import { NativeFeatureIdentifier } from './NativeFeatureIdentifier' import { serverFeatures } from '../Lists/ServerFeatures' import { clientFeatures } from '../Lists/ClientFeatures' import { GetDeprecatedFeatures } from '../Lists/DeprecatedFeatures' @@ -23,11 +23,11 @@ export function GetFeatures(): AnyFeatureDescription[] { ] } -export function FindNativeFeature(identifier: FeatureIdentifier): T | undefined { +export function FindNativeFeature(identifier: string): T | undefined { return GetFeatures().find((f) => f.identifier === identifier) as T } -export function FindNativeTheme(identifier: FeatureIdentifier): ThemeFeatureDescription | undefined { +export function FindNativeTheme(identifier: string): ThemeFeatureDescription | undefined { return themes().find((t) => t.identifier === identifier) } @@ -40,11 +40,11 @@ export function GetIframeEditors(): IframeComponentFeatureDescription[] { } export function GetSuperNoteFeature(): EditorFeatureDescription { - return FindNativeFeature(FeatureIdentifier.SuperEditor) as EditorFeatureDescription + return FindNativeFeature(NativeFeatureIdentifier.TYPES.SuperEditor) as EditorFeatureDescription } export function GetPlainNoteFeature(): EditorFeatureDescription { - return FindNativeFeature(FeatureIdentifier.PlainEditor) as EditorFeatureDescription + return FindNativeFeature(NativeFeatureIdentifier.TYPES.PlainEditor) as EditorFeatureDescription } export function GetNativeThemes(): ThemeFeatureDescription[] { @@ -52,5 +52,5 @@ export function GetNativeThemes(): ThemeFeatureDescription[] { } export function GetDarkThemeFeature(): ThemeFeatureDescription { - return themes().find((t) => t.identifier === FeatureIdentifier.DarkTheme) as ThemeFeatureDescription + return themes().find((t) => t.identifier === NativeFeatureIdentifier.TYPES.DarkTheme) as ThemeFeatureDescription } diff --git a/packages/features/src/Domain/Feature/NativeFeatureIdentifier.ts b/packages/features/src/Domain/Feature/NativeFeatureIdentifier.ts new file mode 100644 index 000000000..f4897d03d --- /dev/null +++ b/packages/features/src/Domain/Feature/NativeFeatureIdentifier.ts @@ -0,0 +1,75 @@ +import { Result, ValueObject } from '@standardnotes/domain-core' + +export interface NativeFeatureIdentifierProps { + value: string +} + +export class NativeFeatureIdentifier extends ValueObject { + static readonly TYPES = { + DailyEmailBackup: 'org.standardnotes.daily-email-backup', + Files: 'org.standardnotes.files', + FilesLowStorageTier: 'org.standardnotes.files-low-storage-tier', + FilesMaximumStorageTier: 'org.standardnotes.files-max-storage-tier', + ListedCustomDomain: 'org.standardnotes.listed-custom-domain', + NoteHistory30Days: 'org.standardnotes.note-history-30', + NoteHistory365Days: 'org.standardnotes.note-history-365', + NoteHistoryUnlimited: 'org.standardnotes.note-history-unlimited', + SignInAlerts: 'com.standardnotes.sign-in-alerts', + SmartFilters: 'org.standardnotes.smart-filters', + TagNesting: 'org.standardnotes.tag-nesting', + TwoFactorAuth: 'org.standardnotes.two-factor-auth', + UniversalSecondFactor: 'org.standardnotes.universal-second-factor', + SubscriptionSharing: 'org.standardnotes.subscription-sharing', + + AutobiographyTheme: 'org.standardnotes.theme-autobiography', + DynamicTheme: 'org.standardnotes.theme-dynamic', + DarkTheme: 'org.standardnotes.theme-focus', + FuturaTheme: 'org.standardnotes.theme-futura', + MidnightTheme: 'org.standardnotes.theme-midnight', + SolarizedDarkTheme: 'org.standardnotes.theme-solarized-dark', + TitaniumTheme: 'org.standardnotes.theme-titanium', + + PlainEditor: 'com.standardnotes.plain-text', + SuperEditor: 'com.standardnotes.super-editor', + + CodeEditor: 'org.standardnotes.code-editor', + MarkdownProEditor: 'org.standardnotes.advanced-markdown-editor', + PlusEditor: 'org.standardnotes.plus-editor', + SheetsEditor: 'org.standardnotes.standard-sheets', + TaskEditor: 'org.standardnotes.simple-task-editor', + TokenVaultEditor: 'org.standardnotes.token-vault', + + Clipper: 'org.standardnotes.clipper', + + DeprecatedMarkdownVisualEditor: 'org.standardnotes.markdown-visual-editor', + DeprecatedBoldEditor: 'org.standardnotes.bold-editor', + DeprecatedMarkdownBasicEditor: 'org.standardnotes.simple-markdown-editor', + DeprecatedMarkdownMathEditor: 'org.standardnotes.fancy-markdown-editor', + DeprecatedMarkdownMinimistEditor: 'org.standardnotes.minimal-markdown-editor', + DeprecatedFoldersComponent: 'org.standardnotes.folders', + DeprecatedFileSafe: 'org.standardnotes.file-safe', + LegacyFileSafeIdentifier: 'org.standardnotes.legacy.file-safe', + } + + get value(): string { + return this.props.value + } + + private constructor(props: NativeFeatureIdentifierProps) { + super(props) + } + + static create(type: string): Result { + const isValidType = Object.values(this.TYPES).includes(type) + if (!isValidType) { + return Result.fail(`Invalid feature identifier: ${type}`) + } else { + return Result.ok(new NativeFeatureIdentifier({ value: type })) + } + } +} + +/** + * Identifier for standalone filesafe instance offered as legacy installable via extensions-server + */ +export const ExperimentalFeatures = [] diff --git a/packages/features/src/Domain/Feature/ServerFeatureDescription.ts b/packages/features/src/Domain/Feature/ServerFeatureDescription.ts index 7b2485faf..6589d275d 100644 --- a/packages/features/src/Domain/Feature/ServerFeatureDescription.ts +++ b/packages/features/src/Domain/Feature/ServerFeatureDescription.ts @@ -1,11 +1,10 @@ import { PermissionName } from '../Permission/PermissionName' -import { FeatureIdentifier } from './FeatureIdentifier' import { RoleFields } from './RoleFields' export type ServerFeatureDescription = RoleFields & { name: string description?: string - identifier: FeatureIdentifier + identifier: string permission_name: PermissionName deprecated?: boolean } diff --git a/packages/features/src/Domain/Lists/ClientFeatures.ts b/packages/features/src/Domain/Lists/ClientFeatures.ts index fa629aece..3308a1376 100644 --- a/packages/features/src/Domain/Lists/ClientFeatures.ts +++ b/packages/features/src/Domain/Lists/ClientFeatures.ts @@ -1,5 +1,5 @@ import { PermissionName } from '../Permission/PermissionName' -import { FeatureIdentifier } from '../Feature/FeatureIdentifier' +import { NativeFeatureIdentifier } from '../Feature/NativeFeatureIdentifier' import { RoleName } from '@standardnotes/domain-core' import { ClientFeatureDescription } from '../Feature/ClientFeatureDescription' @@ -8,7 +8,7 @@ export function clientFeatures(): ClientFeatureDescription[] { { name: 'Tag Nesting', availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], - identifier: FeatureIdentifier.TagNesting, + identifier: NativeFeatureIdentifier.TYPES.TagNesting, permission_name: PermissionName.TagNesting, description: 'Organize your tags into folders.', }, @@ -16,22 +16,22 @@ export function clientFeatures(): ClientFeatureDescription[] { { name: 'Smart Filters', availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], - identifier: FeatureIdentifier.SmartFilters, + identifier: NativeFeatureIdentifier.TYPES.SmartFilters, permission_name: PermissionName.SmartFilters, description: 'Create smart filters for viewing notes matching specific criteria.', }, { name: 'Encrypted files', availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], - identifier: FeatureIdentifier.Files, + identifier: NativeFeatureIdentifier.TYPES.Files, permission_name: PermissionName.Files, description: '', }, { - name: 'Extension', + name: 'Clipper', availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], - identifier: FeatureIdentifier.Extension, - permission_name: PermissionName.Extension, + identifier: NativeFeatureIdentifier.TYPES.Clipper, + permission_name: PermissionName.Clipper, description: '', }, ] diff --git a/packages/features/src/Domain/Lists/DeprecatedFeatures.ts b/packages/features/src/Domain/Lists/DeprecatedFeatures.ts index c491338be..7e21300b1 100644 --- a/packages/features/src/Domain/Lists/DeprecatedFeatures.ts +++ b/packages/features/src/Domain/Lists/DeprecatedFeatures.ts @@ -3,7 +3,7 @@ import { EditorFeatureDescription } from '../Feature/EditorFeatureDescription' import { IframeComponentFeatureDescription } from '../Feature/IframeComponentFeatureDescription' import { ContentType, RoleName } from '@standardnotes/domain-core' import { PermissionName } from '../Permission/PermissionName' -import { FeatureIdentifier } from '../Feature/FeatureIdentifier' +import { NativeFeatureIdentifier } from '../Feature/NativeFeatureIdentifier' import { NoteType } from '../Component/NoteType' import { FillIframeEditorDefaults } from './Utilities/FillEditorComponentDefaults' import { ComponentAction } from '../Component/ComponentAction' @@ -12,7 +12,7 @@ import { ComponentArea } from '../Component/ComponentArea' export function GetDeprecatedFeatures(): AnyFeatureDescription[] { const bold: EditorFeatureDescription = FillIframeEditorDefaults({ name: 'Alternative Rich Text', - identifier: FeatureIdentifier.DeprecatedBoldEditor, + identifier: NativeFeatureIdentifier.TYPES.DeprecatedBoldEditor, note_type: NoteType.RichText, file_type: 'html', component_permissions: [ @@ -39,7 +39,7 @@ export function GetDeprecatedFeatures(): AnyFeatureDescription[] { const markdownBasic: EditorFeatureDescription = FillIframeEditorDefaults({ name: 'Basic Markdown', - identifier: FeatureIdentifier.DeprecatedMarkdownBasicEditor, + identifier: NativeFeatureIdentifier.TYPES.DeprecatedMarkdownBasicEditor, note_type: NoteType.Markdown, spellcheckControl: true, file_type: 'md', @@ -52,7 +52,7 @@ export function GetDeprecatedFeatures(): AnyFeatureDescription[] { const markdownAlt: EditorFeatureDescription = FillIframeEditorDefaults({ name: 'Markdown Alternative', - identifier: FeatureIdentifier.DeprecatedMarkdownVisualEditor, + identifier: NativeFeatureIdentifier.TYPES.DeprecatedMarkdownVisualEditor, note_type: NoteType.Markdown, file_type: 'md', deprecated: true, @@ -66,7 +66,7 @@ export function GetDeprecatedFeatures(): AnyFeatureDescription[] { const markdownMinimist: EditorFeatureDescription = FillIframeEditorDefaults({ name: 'Minimal Markdown', - identifier: FeatureIdentifier.DeprecatedMarkdownMinimistEditor, + identifier: NativeFeatureIdentifier.TYPES.DeprecatedMarkdownMinimistEditor, note_type: NoteType.Markdown, file_type: 'md', index_path: 'index.html', @@ -80,7 +80,7 @@ export function GetDeprecatedFeatures(): AnyFeatureDescription[] { const markdownMath: EditorFeatureDescription = FillIframeEditorDefaults({ name: 'Markdown with Math', - identifier: FeatureIdentifier.DeprecatedMarkdownMathEditor, + identifier: NativeFeatureIdentifier.TYPES.DeprecatedMarkdownMathEditor, spellcheckControl: true, permission_name: PermissionName.MarkdownMathEditor, note_type: NoteType.Markdown, @@ -94,7 +94,7 @@ export function GetDeprecatedFeatures(): AnyFeatureDescription[] { const filesafe: IframeComponentFeatureDescription = FillIframeEditorDefaults({ name: 'FileSafe', - identifier: FeatureIdentifier.DeprecatedFileSafe, + identifier: NativeFeatureIdentifier.TYPES.DeprecatedFileSafe, component_permissions: [ { name: ComponentAction.StreamContextItem, diff --git a/packages/features/src/Domain/Lists/IframeEditors.ts b/packages/features/src/Domain/Lists/IframeEditors.ts index e1be1180a..9256342cc 100644 --- a/packages/features/src/Domain/Lists/IframeEditors.ts +++ b/packages/features/src/Domain/Lists/IframeEditors.ts @@ -1,5 +1,5 @@ import { PermissionName } from '../Permission/PermissionName' -import { FeatureIdentifier } from '../Feature/FeatureIdentifier' +import { NativeFeatureIdentifier } from '../Feature/NativeFeatureIdentifier' import { NoteType } from '../Component/NoteType' import { FillIframeEditorDefaults } from './Utilities/FillEditorComponentDefaults' import { RoleName } from '@standardnotes/domain-core' @@ -9,7 +9,7 @@ export function IframeEditors(): IframeComponentFeatureDescription[] { const code = FillIframeEditorDefaults({ name: 'Code', spellcheckControl: true, - identifier: FeatureIdentifier.CodeEditor, + identifier: NativeFeatureIdentifier.TYPES.CodeEditor, permission_name: PermissionName.CodeEditor, note_type: NoteType.Code, file_type: 'txt', @@ -26,7 +26,7 @@ export function IframeEditors(): IframeComponentFeatureDescription[] { name: 'Rich Text', note_type: NoteType.RichText, file_type: 'html', - identifier: FeatureIdentifier.PlusEditor, + identifier: NativeFeatureIdentifier.TYPES.PlusEditor, permission_name: PermissionName.PlusEditor, spellcheckControl: true, description: @@ -37,7 +37,7 @@ export function IframeEditors(): IframeComponentFeatureDescription[] { const markdown = FillIframeEditorDefaults({ name: 'Markdown', - identifier: FeatureIdentifier.MarkdownProEditor, + identifier: NativeFeatureIdentifier.TYPES.MarkdownProEditor, note_type: NoteType.Markdown, file_type: 'md', permission_name: PermissionName.MarkdownProEditor, @@ -50,7 +50,7 @@ export function IframeEditors(): IframeComponentFeatureDescription[] { const task = FillIframeEditorDefaults({ name: 'Checklist', - identifier: FeatureIdentifier.TaskEditor, + identifier: NativeFeatureIdentifier.TYPES.TaskEditor, note_type: NoteType.Task, spellcheckControl: true, file_type: 'md', @@ -67,7 +67,7 @@ export function IframeEditors(): IframeComponentFeatureDescription[] { note_type: NoteType.Authentication, file_type: 'json', interchangeable: false, - identifier: FeatureIdentifier.TokenVaultEditor, + identifier: NativeFeatureIdentifier.TYPES.TokenVaultEditor, permission_name: PermissionName.TokenVaultEditor, 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.', @@ -77,7 +77,7 @@ export function IframeEditors(): IframeComponentFeatureDescription[] { const spreadsheets = FillIframeEditorDefaults({ name: 'Spreadsheet', - identifier: FeatureIdentifier.SheetsEditor, + identifier: NativeFeatureIdentifier.TYPES.SheetsEditor, note_type: NoteType.Spreadsheet, file_type: 'json', interchangeable: false, diff --git a/packages/features/src/Domain/Lists/NativeEditors.ts b/packages/features/src/Domain/Lists/NativeEditors.ts index f05cc6083..71620bc39 100644 --- a/packages/features/src/Domain/Lists/NativeEditors.ts +++ b/packages/features/src/Domain/Lists/NativeEditors.ts @@ -1,7 +1,7 @@ import { RoleName } from '@standardnotes/domain-core' import { NoteType } from '../Component/NoteType' import { EditorFeatureDescription } from '../Feature/EditorFeatureDescription' -import { FeatureIdentifier } from '../Feature/FeatureIdentifier' +import { NativeFeatureIdentifier } from '../Feature/NativeFeatureIdentifier' import { PermissionName } from '../Permission/PermissionName' export function nativeEditors(): EditorFeatureDescription[] { @@ -9,7 +9,7 @@ export function nativeEditors(): EditorFeatureDescription[] { { name: 'Super', note_type: NoteType.Super, - identifier: FeatureIdentifier.SuperEditor, + identifier: NativeFeatureIdentifier.TYPES.SuperEditor, spellcheckControl: true, file_type: 'json', interchangeable: false, @@ -24,7 +24,7 @@ export function nativeEditors(): EditorFeatureDescription[] { spellcheckControl: true, file_type: 'txt', interchangeable: true, - identifier: FeatureIdentifier.PlainEditor, + identifier: NativeFeatureIdentifier.TYPES.PlainEditor, availableInRoles: [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], permission_name: PermissionName.PlainEditor, }, diff --git a/packages/features/src/Domain/Lists/ServerFeatures.ts b/packages/features/src/Domain/Lists/ServerFeatures.ts index f54be0b5a..b8f2b10b6 100644 --- a/packages/features/src/Domain/Lists/ServerFeatures.ts +++ b/packages/features/src/Domain/Lists/ServerFeatures.ts @@ -1,67 +1,67 @@ import { ServerFeatureDescription } from '../Feature/ServerFeatureDescription' import { PermissionName } from '../Permission/PermissionName' -import { FeatureIdentifier } from '../Feature/FeatureIdentifier' +import { NativeFeatureIdentifier } from '../Feature/NativeFeatureIdentifier' import { RoleName } from '@standardnotes/domain-core' export function serverFeatures(): ServerFeatureDescription[] { return [ { name: 'Two factor authentication', - identifier: FeatureIdentifier.TwoFactorAuth, + identifier: NativeFeatureIdentifier.TYPES.TwoFactorAuth, permission_name: PermissionName.TwoFactorAuth, availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], }, { name: 'U2F authentication', - identifier: FeatureIdentifier.UniversalSecondFactor, + identifier: NativeFeatureIdentifier.TYPES.UniversalSecondFactor, permission_name: PermissionName.UniversalSecondFactor, availableInRoles: [RoleName.NAMES.ProUser], }, { name: 'Unlimited note history', - identifier: FeatureIdentifier.NoteHistoryUnlimited, + identifier: NativeFeatureIdentifier.TYPES.NoteHistoryUnlimited, permission_name: PermissionName.NoteHistoryUnlimited, availableInRoles: [RoleName.NAMES.ProUser], }, { name: '365 days note history', - identifier: FeatureIdentifier.NoteHistory365Days, + identifier: NativeFeatureIdentifier.TYPES.NoteHistory365Days, permission_name: PermissionName.NoteHistory365Days, availableInRoles: [RoleName.NAMES.PlusUser], }, { name: 'Email backups', - identifier: FeatureIdentifier.DailyEmailBackup, + identifier: NativeFeatureIdentifier.TYPES.DailyEmailBackup, permission_name: PermissionName.DailyEmailBackup, availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], }, { name: 'Sign-in email alerts', - identifier: FeatureIdentifier.SignInAlerts, + identifier: NativeFeatureIdentifier.TYPES.SignInAlerts, permission_name: PermissionName.SignInAlerts, availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], }, { name: 'Files maximum storage tier', - identifier: FeatureIdentifier.FilesMaximumStorageTier, + identifier: NativeFeatureIdentifier.TYPES.FilesMaximumStorageTier, permission_name: PermissionName.FilesMaximumStorageTier, availableInRoles: [RoleName.NAMES.ProUser], }, { name: 'Files low storage tier', - identifier: FeatureIdentifier.FilesLowStorageTier, + identifier: NativeFeatureIdentifier.TYPES.FilesLowStorageTier, permission_name: PermissionName.FilesLowStorageTier, availableInRoles: [RoleName.NAMES.PlusUser], }, { name: 'Files medium storage tier', - identifier: FeatureIdentifier.SubscriptionSharing, + identifier: NativeFeatureIdentifier.TYPES.SubscriptionSharing, permission_name: PermissionName.SubscriptionSharing, availableInRoles: [RoleName.NAMES.ProUser], }, { name: 'Listed Custom Domain', - identifier: FeatureIdentifier.ListedCustomDomain, + identifier: NativeFeatureIdentifier.TYPES.ListedCustomDomain, permission_name: PermissionName.ListedCustomDomain, availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], }, diff --git a/packages/features/src/Domain/Lists/Themes.ts b/packages/features/src/Domain/Lists/Themes.ts index 9876242d6..cd80db474 100644 --- a/packages/features/src/Domain/Lists/Themes.ts +++ b/packages/features/src/Domain/Lists/Themes.ts @@ -1,13 +1,13 @@ import { ThemeFeatureDescription } from '../Feature/ThemeFeatureDescription' import { PermissionName } from '../Permission/PermissionName' -import { FeatureIdentifier } from '../Feature/FeatureIdentifier' +import { NativeFeatureIdentifier } from '../Feature/NativeFeatureIdentifier' import { FillThemeComponentDefaults } from './Utilities/FillThemeComponentDefaults' import { RoleName } from '@standardnotes/domain-core' export function themes(): ThemeFeatureDescription[] { const midnight: ThemeFeatureDescription = FillThemeComponentDefaults({ name: 'Midnight', - identifier: FeatureIdentifier.MidnightTheme, + identifier: NativeFeatureIdentifier.TYPES.MidnightTheme, permission_name: PermissionName.MidnightTheme, isDark: true, dock_icon: { @@ -22,7 +22,7 @@ export function themes(): ThemeFeatureDescription[] { const futura: ThemeFeatureDescription = FillThemeComponentDefaults({ availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], name: 'Futura', - identifier: FeatureIdentifier.FuturaTheme, + identifier: NativeFeatureIdentifier.TYPES.FuturaTheme, permission_name: PermissionName.FuturaTheme, isDark: true, dock_icon: { @@ -36,7 +36,7 @@ export function themes(): ThemeFeatureDescription[] { const solarizedDark: ThemeFeatureDescription = FillThemeComponentDefaults({ availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], name: 'Solarized Dark', - identifier: FeatureIdentifier.SolarizedDarkTheme, + identifier: NativeFeatureIdentifier.TYPES.SolarizedDarkTheme, permission_name: PermissionName.SolarizedDarkTheme, isDark: true, dock_icon: { @@ -50,7 +50,7 @@ export function themes(): ThemeFeatureDescription[] { const autobiography: ThemeFeatureDescription = FillThemeComponentDefaults({ availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], name: 'Autobiography', - identifier: FeatureIdentifier.AutobiographyTheme, + identifier: NativeFeatureIdentifier.TYPES.AutobiographyTheme, permission_name: PermissionName.AutobiographyTheme, dock_icon: { type: 'circle', @@ -63,7 +63,7 @@ export function themes(): ThemeFeatureDescription[] { const dark: ThemeFeatureDescription = FillThemeComponentDefaults({ availableInRoles: [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], name: 'Dark', - identifier: FeatureIdentifier.DarkTheme, + identifier: NativeFeatureIdentifier.TYPES.DarkTheme, permission_name: PermissionName.FocusedTheme, clientControlled: true, isDark: true, @@ -78,7 +78,7 @@ export function themes(): ThemeFeatureDescription[] { const titanium: ThemeFeatureDescription = FillThemeComponentDefaults({ availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], name: 'Titanium', - identifier: FeatureIdentifier.TitaniumTheme, + identifier: NativeFeatureIdentifier.TYPES.TitaniumTheme, permission_name: PermissionName.TitaniumTheme, dock_icon: { type: 'circle', @@ -91,7 +91,7 @@ export function themes(): ThemeFeatureDescription[] { const dynamic: ThemeFeatureDescription = FillThemeComponentDefaults({ availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser], name: 'Dynamic Panels', - identifier: FeatureIdentifier.DynamicTheme, + identifier: NativeFeatureIdentifier.TYPES.DynamicTheme, permission_name: PermissionName.ThemeDynamic, layerable: true, no_mobile: true, diff --git a/packages/features/src/Domain/Permission/PermissionName.ts b/packages/features/src/Domain/Permission/PermissionName.ts index f87773493..9f03b3864 100644 --- a/packages/features/src/Domain/Permission/PermissionName.ts +++ b/packages/features/src/Domain/Permission/PermissionName.ts @@ -37,5 +37,5 @@ export enum PermissionName { UniversalSecondFactor = 'server:universal-second-factor', SubscriptionSharing = 'server:subscription-sharing', SuperEditor = 'editor:super-editor', - Extension = 'app:extension', + Clipper = 'app:clipper', } diff --git a/packages/features/src/Domain/index.ts b/packages/features/src/Domain/index.ts index 8d3ff6092..e80f4f693 100644 --- a/packages/features/src/Domain/index.ts +++ b/packages/features/src/Domain/index.ts @@ -1,5 +1,5 @@ export * from './Feature/AnyFeatureDescription' -export * from './Feature/FeatureIdentifier' +export * from './Feature/NativeFeatureIdentifier' export * from './Feature/Features' export * from './Feature/TypeGuards' @@ -22,4 +22,3 @@ export * from './Component/ComponentFlag' export * from './Component/ComponentPermission' export * from './Component/NoteType' export * from './Component/ThemeDockIcon' -export * from './Component/EditorIdentifier' diff --git a/packages/models/src/Domain/Runtime/Feature/TypeGuards.spec.ts b/packages/models/src/Domain/Runtime/Feature/TypeGuards.spec.ts index 5e8a8a00a..225640ba7 100644 --- a/packages/models/src/Domain/Runtime/Feature/TypeGuards.spec.ts +++ b/packages/models/src/Domain/Runtime/Feature/TypeGuards.spec.ts @@ -6,11 +6,7 @@ import { NoteType, UIFeatureDescriptionTypes, } from '@standardnotes/features' -import { - isUIFeatureAnIframeFeature, - isComponentOrFeatureDescriptionAComponent, - isComponentOrFeatureDescriptionAFeatureDescription, -} from './TypeGuards' +import { isUIFeatureAnIframeFeature, isItemBasedFeature, isNativeFeature } from './TypeGuards' import { UIFeature } from './UIFeature' import { ComponentInterface } from '../../Syncable/Component' import { ContentType } from '@standardnotes/domain-core' @@ -45,7 +41,7 @@ describe('TypeGuards', () => { uuid: 'abc-123', } as ComponentInterface - expect(isComponentOrFeatureDescriptionAComponent(x)).toBe(true) + expect(isItemBasedFeature(x)).toBe(true) }) it('should return false if feature description is not a component', () => { @@ -53,17 +49,17 @@ describe('TypeGuards', () => { note_type: NoteType.Super, } as jest.Mocked - expect(isComponentOrFeatureDescriptionAComponent(x)).toBe(false) + expect(isItemBasedFeature(x)).toBe(false) }) }) - describe('isComponentOrFeatureDescriptionAFeatureDescription', () => { + describe('isNativeFeature', () => { it('should return true if x is a feature description', () => { const x: AnyFeatureDescription = { content_type: 'TestContentType', } as AnyFeatureDescription - expect(isComponentOrFeatureDescriptionAFeatureDescription(x)).toBe(true) + expect(isNativeFeature(x)).toBe(true) }) it('should return false if x is a component', () => { @@ -71,7 +67,7 @@ describe('TypeGuards', () => { uuid: 'abc-123', } as ComponentInterface - expect(isComponentOrFeatureDescriptionAFeatureDescription(x)).toBe(false) + expect(isNativeFeature(x)).toBe(false) }) }) }) diff --git a/packages/models/src/Domain/Runtime/Feature/TypeGuards.ts b/packages/models/src/Domain/Runtime/Feature/TypeGuards.ts index e79d83660..bde2f63a7 100644 --- a/packages/models/src/Domain/Runtime/Feature/TypeGuards.ts +++ b/packages/models/src/Domain/Runtime/Feature/TypeGuards.ts @@ -14,14 +14,10 @@ export function isUIFeatureAnIframeFeature( return isIframeComponentFeatureDescription(x.featureDescription) } -export function isComponentOrFeatureDescriptionAComponent( - x: ComponentInterface | UIFeatureDescriptionTypes, -): x is ComponentInterface { +export function isItemBasedFeature(x: ComponentInterface | UIFeatureDescriptionTypes): x is ComponentInterface { return 'uuid' in x } -export function isComponentOrFeatureDescriptionAFeatureDescription( - x: ComponentInterface | AnyFeatureDescription, -): x is AnyFeatureDescription { +export function isNativeFeature(x: ComponentInterface | AnyFeatureDescription): x is AnyFeatureDescription { return !('uuid' in x) } diff --git a/packages/models/src/Domain/Runtime/Feature/UIFeature.ts b/packages/models/src/Domain/Runtime/Feature/UIFeature.ts index bda5c3ad3..18abf2409 100644 --- a/packages/models/src/Domain/Runtime/Feature/UIFeature.ts +++ b/packages/models/src/Domain/Runtime/Feature/UIFeature.ts @@ -2,7 +2,7 @@ import { ComponentArea, ComponentPermission, EditorFeatureDescription, - FeatureIdentifier, + NativeFeatureIdentifier, NoteType, ThemeDockIcon, UIFeatureDescriptionTypes, @@ -12,29 +12,27 @@ import { } from '@standardnotes/features' import { ComponentInterface } from '../../Syncable/Component/ComponentInterface' import { isTheme } from '../../Syncable/Theme' -import { - isComponentOrFeatureDescriptionAComponent, - isComponentOrFeatureDescriptionAFeatureDescription, -} from './TypeGuards' +import { isItemBasedFeature, isNativeFeature } from './TypeGuards' import { UIFeatureInterface } from './UIFeatureInterface' +import { Uuid } from '@standardnotes/domain-core' export class UIFeature implements UIFeatureInterface { constructor(public readonly item: ComponentInterface | F) {} get isComponent(): boolean { - return isComponentOrFeatureDescriptionAComponent(this.item) + return isItemBasedFeature(this.item) } get isFeatureDescription(): boolean { - return isComponentOrFeatureDescriptionAFeatureDescription(this.item) + return isNativeFeature(this.item) } get isThemeComponent(): boolean { - return isComponentOrFeatureDescriptionAComponent(this.item) && isTheme(this.item) + return isItemBasedFeature(this.item) && isTheme(this.item) } get asComponent(): ComponentInterface { - if (isComponentOrFeatureDescriptionAComponent(this.item)) { + if (isItemBasedFeature(this.item)) { return this.item } @@ -42,29 +40,30 @@ export class UIFeature implements UIFeature } get asFeatureDescription(): F { - if (isComponentOrFeatureDescriptionAFeatureDescription(this.item)) { + if (isNativeFeature(this.item)) { return this.item } throw new Error('Cannot cast item to feature description') } - get uniqueIdentifier(): string { - if (isComponentOrFeatureDescriptionAFeatureDescription(this.item)) { - return this.item.identifier + get uniqueIdentifier(): NativeFeatureIdentifier | Uuid { + if (isNativeFeature(this.item)) { + const nativeFeature = NativeFeatureIdentifier.create(this.item.identifier) + return nativeFeature.getValue() } else { - return this.item.uuid + return Uuid.create(this.item.uuid).getValue() } } - get featureIdentifier(): FeatureIdentifier { + get featureIdentifier(): string { return this.item.identifier } get noteType(): NoteType { - if (isComponentOrFeatureDescriptionAFeatureDescription(this.item) && isEditorFeatureDescription(this.item)) { + if (isNativeFeature(this.item) && isEditorFeatureDescription(this.item)) { return this.item.note_type ?? NoteType.Unknown - } else if (isComponentOrFeatureDescriptionAComponent(this.item)) { + } else if (isItemBasedFeature(this.item)) { return this.item.noteType } @@ -72,12 +71,9 @@ export class UIFeature implements UIFeature } get fileType(): EditorFeatureDescription['file_type'] { - if (isComponentOrFeatureDescriptionAFeatureDescription(this.item) && isEditorFeatureDescription(this.item)) { + if (isNativeFeature(this.item) && isEditorFeatureDescription(this.item)) { return this.item.file_type - } else if ( - isComponentOrFeatureDescriptionAComponent(this.item) && - isEditorFeatureDescription(this.item.package_info) - ) { + } else if (isItemBasedFeature(this.item) && isEditorFeatureDescription(this.item.package_info)) { return this.item.package_info?.file_type ?? 'txt' } @@ -85,7 +81,7 @@ export class UIFeature implements UIFeature } get displayName(): string { - if (isComponentOrFeatureDescriptionAFeatureDescription(this.item)) { + if (isNativeFeature(this.item)) { return this.item.name ?? '' } else { return this.item.displayName @@ -93,7 +89,7 @@ export class UIFeature implements UIFeature } get description(): string { - if (isComponentOrFeatureDescriptionAFeatureDescription(this.item)) { + if (isNativeFeature(this.item)) { return this.item.description ?? '' } else { return this.item.package_info.description ?? '' @@ -101,7 +97,7 @@ export class UIFeature implements UIFeature } get deprecationMessage(): string | undefined { - if (isComponentOrFeatureDescriptionAFeatureDescription(this.item)) { + if (isNativeFeature(this.item)) { return this.item.deprecation_message } else { return this.item.deprecationMessage @@ -109,7 +105,7 @@ export class UIFeature implements UIFeature } get expirationDate(): Date | undefined { - if (isComponentOrFeatureDescriptionAFeatureDescription(this.item)) { + if (isNativeFeature(this.item)) { return this.item.expires_at ? new Date(this.item.expires_at) : undefined } else { return this.item.valid_until @@ -117,7 +113,7 @@ export class UIFeature implements UIFeature } get featureDescription(): F { - if (isComponentOrFeatureDescriptionAFeatureDescription(this.item)) { + if (isNativeFeature(this.item)) { return this.item } else { return this.item.package_info as F @@ -125,12 +121,9 @@ export class UIFeature implements UIFeature } get acquiredPermissions(): ComponentPermission[] { - if ( - isComponentOrFeatureDescriptionAFeatureDescription(this.item) && - isIframeComponentFeatureDescription(this.item) - ) { + if (isNativeFeature(this.item) && isIframeComponentFeatureDescription(this.item)) { return this.item.component_permissions ?? [] - } else if (isComponentOrFeatureDescriptionAComponent(this.item)) { + } else if (isItemBasedFeature(this.item)) { return this.item.permissions } @@ -146,7 +139,7 @@ export class UIFeature implements UIFeature } get layerable(): boolean { - if (isComponentOrFeatureDescriptionAComponent(this.item) && isTheme(this.item)) { + if (isItemBasedFeature(this.item) && isTheme(this.item)) { return this.item.layerable } else if (isThemeFeatureDescription(this.asFeatureDescription)) { return this.asFeatureDescription.layerable ?? false @@ -156,7 +149,7 @@ export class UIFeature implements UIFeature } get dockIcon(): ThemeDockIcon | undefined { - if (isComponentOrFeatureDescriptionAComponent(this.item) && isTheme(this.item)) { + if (isItemBasedFeature(this.item) && isTheme(this.item)) { return this.item.package_info.dock_icon } else if (isThemeFeatureDescription(this.asFeatureDescription)) { return this.asFeatureDescription.dock_icon diff --git a/packages/models/src/Domain/Runtime/Feature/UIFeatureInterface.ts b/packages/models/src/Domain/Runtime/Feature/UIFeatureInterface.ts index 61c87e9e9..894d2b2d3 100644 --- a/packages/models/src/Domain/Runtime/Feature/UIFeatureInterface.ts +++ b/packages/models/src/Domain/Runtime/Feature/UIFeatureInterface.ts @@ -2,12 +2,13 @@ import { ComponentArea, ComponentPermission, EditorFeatureDescription, - FeatureIdentifier, + NativeFeatureIdentifier, NoteType, ThemeDockIcon, UIFeatureDescriptionTypes, } from '@standardnotes/features' import { ComponentInterface } from '../../Syncable/Component' +import { Uuid } from '@standardnotes/domain-core' export interface UIFeatureInterface { item: ComponentInterface | F @@ -16,8 +17,8 @@ export interface UIFeatureInterface { get isThemeComponent(): boolean get asComponent(): ComponentInterface get asFeatureDescription(): F - get uniqueIdentifier(): string - get featureIdentifier(): FeatureIdentifier + get uniqueIdentifier(): NativeFeatureIdentifier | Uuid + get featureIdentifier(): string get noteType(): NoteType get fileType(): EditorFeatureDescription['file_type'] get displayName(): string diff --git a/packages/models/src/Domain/Syncable/Component/Component.ts b/packages/models/src/Domain/Syncable/Component/Component.ts index b1967c601..b4456eb7f 100644 --- a/packages/models/src/Domain/Syncable/Component/Component.ts +++ b/packages/models/src/Domain/Syncable/Component/Component.ts @@ -1,6 +1,5 @@ import { isValidUrl } from '@standardnotes/utils' import { - FeatureIdentifier, ThirdPartyFeatureDescription, ComponentArea, ComponentFlag, @@ -175,7 +174,7 @@ export class SNComponent extends DecryptedItem implements Comp return this.valid_until.getTime() > 0 && this.valid_until <= new Date() } - public get identifier(): FeatureIdentifier { + public get identifier(): string { return this.package_info.identifier } diff --git a/packages/models/src/Domain/Syncable/Component/ComponentInterface.ts b/packages/models/src/Domain/Syncable/Component/ComponentInterface.ts index 4b2c73375..81af7cfbb 100644 --- a/packages/models/src/Domain/Syncable/Component/ComponentInterface.ts +++ b/packages/models/src/Domain/Syncable/Component/ComponentInterface.ts @@ -1,10 +1,4 @@ -import { - ComponentArea, - ComponentPermission, - FeatureIdentifier, - NoteType, - ThirdPartyFeatureDescription, -} from '@standardnotes/features' +import { ComponentArea, ComponentPermission, NoteType, ThirdPartyFeatureDescription } from '@standardnotes/features' import { ComponentPackageInfo } from './PackageInfo' import { DecryptedItemInterface } from '../../Abstract/Item' import { ComponentContent } from './ComponentContent' @@ -35,7 +29,7 @@ export interface ComponentInterface extends DecryptedItemInterface implements NoteContentSpe public readonly authorizedForListed: boolean /** The package_info.identifier of the editor (not its uuid), such as org.standardnotes.advanced-markdown */ - public readonly editorIdentifier?: FeatureIdentifier | string + public readonly editorIdentifier?: string constructor(payload: DecryptedPayloadInterface) { super(payload) diff --git a/packages/models/src/Domain/Syncable/Note/NoteContent.ts b/packages/models/src/Domain/Syncable/Note/NoteContent.ts index 21abafc1b..8a5c6540c 100644 --- a/packages/models/src/Domain/Syncable/Note/NoteContent.ts +++ b/packages/models/src/Domain/Syncable/Note/NoteContent.ts @@ -1,4 +1,4 @@ -import { FeatureIdentifier, NoteType } from '@standardnotes/features' +import { NoteType } from '@standardnotes/features' import { ItemContent } from '../../Abstract/Content/ItemContent' import { EditorLineWidth } from '../UserPrefs' @@ -11,7 +11,7 @@ export interface NoteContentSpecialized { spellcheck?: boolean editorWidth?: EditorLineWidth noteType?: NoteType - editorIdentifier?: FeatureIdentifier | string + editorIdentifier?: string authorizedForListed?: boolean } diff --git a/packages/models/src/Domain/Syncable/Note/NoteMutator.spec.ts b/packages/models/src/Domain/Syncable/Note/NoteMutator.spec.ts index 8167f7811..c07e22356 100644 --- a/packages/models/src/Domain/Syncable/Note/NoteMutator.spec.ts +++ b/packages/models/src/Domain/Syncable/Note/NoteMutator.spec.ts @@ -1,7 +1,7 @@ import { NoteMutator } from './NoteMutator' import { createNote } from './../../Utilities/Test/SpecUtils' import { MutationType } from '../../Abstract/Item' -import { FeatureIdentifier, NoteType } from '@standardnotes/features' +import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features' describe('note mutator', () => { it('sets noteType', () => { @@ -16,9 +16,9 @@ describe('note mutator', () => { it('sets componentIdentifier', () => { const note = createNote({}) const mutator = new NoteMutator(note, MutationType.NoUpdateUserTimestamps) - mutator.editorIdentifier = FeatureIdentifier.MarkdownProEditor + mutator.editorIdentifier = NativeFeatureIdentifier.TYPES.MarkdownProEditor const result = mutator.getResult() - expect(result.content.editorIdentifier).toEqual(FeatureIdentifier.MarkdownProEditor) + expect(result.content.editorIdentifier).toEqual(NativeFeatureIdentifier.TYPES.MarkdownProEditor) }) }) diff --git a/packages/models/src/Domain/Syncable/Note/NoteMutator.ts b/packages/models/src/Domain/Syncable/Note/NoteMutator.ts index 3f08cd392..186f4a229 100644 --- a/packages/models/src/Domain/Syncable/Note/NoteMutator.ts +++ b/packages/models/src/Domain/Syncable/Note/NoteMutator.ts @@ -3,7 +3,7 @@ import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemM import { SNNote } from './Note' import { NoteToNoteReference } from '../../Abstract/Reference/NoteToNoteReference' import { ContentReferenceType } from '../../Abstract/Item' -import { FeatureIdentifier, NoteType } from '@standardnotes/features' +import { NoteType } from '@standardnotes/features' import { EditorLineWidth } from '../UserPrefs' import { ContentType } from '@standardnotes/domain-core' @@ -40,7 +40,7 @@ export class NoteMutator extends DecryptedItemMutator { this.mutableContent.noteType = noteType } - set editorIdentifier(identifier: FeatureIdentifier | string | undefined) { + set editorIdentifier(identifier: string | undefined) { this.mutableContent.editorIdentifier = identifier } diff --git a/packages/models/src/Domain/Syncable/Tag/TagPreferences.ts b/packages/models/src/Domain/Syncable/Tag/TagPreferences.ts index 2fc70d269..3c7726978 100644 --- a/packages/models/src/Domain/Syncable/Tag/TagPreferences.ts +++ b/packages/models/src/Domain/Syncable/Tag/TagPreferences.ts @@ -1,4 +1,3 @@ -import { EditorIdentifier } from '@standardnotes/features' import { NewNoteTitleFormat } from '../UserPrefs' import { CollectionSortProperty } from './../../Runtime/Collection/CollectionSort' @@ -15,7 +14,7 @@ export interface TagPreferences { hideEditorIcon?: boolean newNoteTitleFormat?: NewNoteTitleFormat customNoteTitleFormat?: string - editorIdentifier?: EditorIdentifier + editorIdentifier?: string entryMode?: 'normal' | 'daily' panelWidth?: number useTableView?: boolean diff --git a/packages/models/src/Domain/Syncable/UserPrefs/ComponentPreferences.ts b/packages/models/src/Domain/Syncable/UserPrefs/ComponentPreferences.ts index 0b13b6f58..a4825e5e4 100644 --- a/packages/models/src/Domain/Syncable/UserPrefs/ComponentPreferences.ts +++ b/packages/models/src/Domain/Syncable/UserPrefs/ComponentPreferences.ts @@ -1,7 +1,3 @@ -import { FeatureIdentifier } from '@standardnotes/features' - -type UuidString = string - -export type AllComponentPreferences = Record +export type AllComponentPreferences = Record export type ComponentPreferencesEntry = Record diff --git a/packages/models/src/Domain/Syncable/UserPrefs/PrefDefaults.ts b/packages/models/src/Domain/Syncable/UserPrefs/PrefDefaults.ts index b1e27095c..6f054f896 100644 --- a/packages/models/src/Domain/Syncable/UserPrefs/PrefDefaults.ts +++ b/packages/models/src/Domain/Syncable/UserPrefs/PrefDefaults.ts @@ -1,4 +1,4 @@ -import { FeatureIdentifier } from '@standardnotes/features' +import { NativeFeatureIdentifier } from '@standardnotes/features' import { CollectionSort } from '../../Runtime/Collection/CollectionSort' import { EditorFontSize } from './EditorFontSize' import { EditorLineHeight } from './EditorLineHeight' @@ -29,7 +29,7 @@ export const PrefDefaults = { [PrefKey.NotesHideEditorIcon]: false, [PrefKey.UseSystemColorScheme]: false, [PrefKey.AutoLightThemeIdentifier]: 'Default', - [PrefKey.AutoDarkThemeIdentifier]: FeatureIdentifier.DarkTheme, + [PrefKey.AutoDarkThemeIdentifier]: NativeFeatureIdentifier.TYPES.DarkTheme, [PrefKey.NoteAddToParentFolders]: true, [PrefKey.NewNoteTitleFormat]: NewNoteTitleFormat.CurrentDateAndTime, [PrefKey.CustomNoteTitleFormat]: 'YYYY-MM-DD [at] hh:mm A', @@ -37,7 +37,7 @@ export const PrefDefaults = { [PrefKey.PaneGesturesEnabled]: true, [PrefKey.MomentsDefaultTagUuid]: undefined, [PrefKey.ClipperDefaultTagUuid]: undefined, - [PrefKey.DefaultEditorIdentifier]: FeatureIdentifier.PlainEditor, + [PrefKey.DefaultEditorIdentifier]: NativeFeatureIdentifier.TYPES.PlainEditor, [PrefKey.SuperNoteExportFormat]: 'json', [PrefKey.SystemViewPreferences]: {}, [PrefKey.AuthenticatorNames]: '', diff --git a/packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts b/packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts index 4f151edad..51c34ac2f 100644 --- a/packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts +++ b/packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts @@ -1,5 +1,4 @@ import { CollectionSortProperty } from '../../Runtime/Collection/CollectionSort' -import { EditorIdentifier, FeatureIdentifier } from '@standardnotes/features' import { SystemViewId } from '../SmartView' import { TagPreferences } from '../Tag' import { NewNoteTitleFormat } from './NewNoteTitleFormat' @@ -67,8 +66,8 @@ export type PrefValue = { [PrefKey.NotesHideTags]: boolean [PrefKey.NotesHideEditorIcon]: boolean [PrefKey.UseSystemColorScheme]: boolean - [PrefKey.AutoLightThemeIdentifier]: FeatureIdentifier | 'Default' | 'Dark' - [PrefKey.AutoDarkThemeIdentifier]: FeatureIdentifier | 'Default' | 'Dark' + [PrefKey.AutoLightThemeIdentifier]: string + [PrefKey.AutoDarkThemeIdentifier]: string [PrefKey.NoteAddToParentFolders]: boolean [PrefKey.NewNoteTitleFormat]: NewNoteTitleFormat [PrefKey.CustomNoteTitleFormat]: string @@ -76,7 +75,7 @@ export type PrefValue = { [PrefKey.EditorLineWidth]: EditorLineWidth [PrefKey.EditorFontSize]: EditorFontSize [PrefKey.UpdateSavingStatusIndicator]: boolean - [PrefKey.DefaultEditorIdentifier]: EditorIdentifier + [PrefKey.DefaultEditorIdentifier]: string [PrefKey.MomentsDefaultTagUuid]: string | undefined [PrefKey.ClipperDefaultTagUuid]: string | undefined [PrefKey.SystemViewPreferences]: Partial> diff --git a/packages/services/src/Domain/Component/ComponentManagerInterface.ts b/packages/services/src/Domain/Component/ComponentManagerInterface.ts index f19ce9dcf..8461e29b8 100644 --- a/packages/services/src/Domain/Component/ComponentManagerInterface.ts +++ b/packages/services/src/Domain/Component/ComponentManagerInterface.ts @@ -3,13 +3,14 @@ import { ComponentArea, ComponentFeatureDescription, EditorFeatureDescription, - EditorIdentifier, IframeComponentFeatureDescription, + NativeFeatureIdentifier, ThemeFeatureDescription, } from '@standardnotes/features' import { ActionObserver, ComponentInterface, UIFeature, PermissionDialog, SNNote, SNTag } from '@standardnotes/models' import { DesktopManagerInterface } from '../Device/DesktopManagerInterface' import { ComponentViewerInterface } from './ComponentViewerInterface' +import { Uuid } from '@standardnotes/domain-core' export interface ComponentManagerInterface { urlForFeature(uiFeature: UIFeature): string | undefined @@ -31,12 +32,12 @@ export interface ComponentManagerInterface { setPermissionDialogUIHandler(handler: (dialog: PermissionDialog) => void): void editorForNote(note: SNNote): UIFeature - getDefaultEditorIdentifier(currentTag?: SNTag): EditorIdentifier + getDefaultEditorIdentifier(currentTag?: SNTag): string isThemeActive(theme: UIFeature): boolean toggleTheme(theme: UIFeature): Promise getActiveThemes(): UIFeature[] - getActiveThemesIdentifiers(): string[] + getActiveThemesIdentifiers(): { features: NativeFeatureIdentifier[]; uuids: Uuid[] } isComponentActive(component: ComponentInterface): boolean toggleComponent(component: ComponentInterface): Promise diff --git a/packages/services/src/Domain/Component/ComponentViewerInterface.ts b/packages/services/src/Domain/Component/ComponentViewerInterface.ts index a5784a8a1..fe0fd0b25 100644 --- a/packages/services/src/Domain/Component/ComponentViewerInterface.ts +++ b/packages/services/src/Domain/Component/ComponentViewerInterface.ts @@ -1,7 +1,8 @@ import { ActionObserver, ComponentEventObserver, ComponentMessage, UIFeature } from '@standardnotes/models' import { FeatureStatus } from '../Feature/FeatureStatus' import { ComponentViewerError } from './ComponentViewerError' -import { IframeComponentFeatureDescription } from '@standardnotes/features' +import { IframeComponentFeatureDescription, NativeFeatureIdentifier } from '@standardnotes/features' +import { Uuid } from '@standardnotes/domain-core' export interface ComponentViewerInterface { readonly identifier: string @@ -9,7 +10,7 @@ export interface ComponentViewerInterface { readonly sessionKey?: string get url(): string - get componentUniqueIdentifier(): string + get componentUniqueIdentifier(): NativeFeatureIdentifier | Uuid getComponentOrFeatureItem(): UIFeature diff --git a/packages/services/src/Domain/Feature/FeaturesClientInterface.ts b/packages/services/src/Domain/Feature/FeaturesClientInterface.ts index 0b9435fcb..362ec6cbd 100644 --- a/packages/services/src/Domain/Feature/FeaturesClientInterface.ts +++ b/packages/services/src/Domain/Feature/FeaturesClientInterface.ts @@ -1,12 +1,15 @@ -import { FeatureIdentifier } from '@standardnotes/features' import { ComponentInterface, DecryptedItemInterface } from '@standardnotes/models' import { FeatureStatus } from './FeatureStatus' import { SetOfflineFeaturesFunctionResponse } from './SetOfflineFeaturesFunctionResponse' +import { NativeFeatureIdentifier } from '@standardnotes/features' +import { Uuid } from '@standardnotes/domain-core' export interface FeaturesClientInterface { - initializeFromDisk(): void - getFeatureStatus(featureId: FeatureIdentifier, options?: { inContextOfItem?: DecryptedItemInterface }): FeatureStatus + getFeatureStatus( + featureId: NativeFeatureIdentifier | Uuid, + options?: { inContextOfItem?: DecryptedItemInterface }, + ): FeatureStatus hasMinimumRole(role: string): boolean hasFirstPartyOfflineSubscription(): boolean @@ -16,13 +19,13 @@ export interface FeaturesClientInterface { isThirdPartyFeature(identifier: string): boolean - toggleExperimentalFeature(identifier: FeatureIdentifier): void - getExperimentalFeatures(): FeatureIdentifier[] - getEnabledExperimentalFeatures(): FeatureIdentifier[] - enableExperimentalFeature(identifier: FeatureIdentifier): void - disableExperimentalFeature(identifier: FeatureIdentifier): void - isExperimentalFeatureEnabled(identifier: FeatureIdentifier): boolean - isExperimentalFeature(identifier: FeatureIdentifier): boolean + toggleExperimentalFeature(identifier: string): void + getExperimentalFeatures(): string[] + getEnabledExperimentalFeatures(): string[] + enableExperimentalFeature(identifier: string): void + disableExperimentalFeature(identifier: string): void + isExperimentalFeatureEnabled(identifier: string): boolean + isExperimentalFeature(identifier: string): boolean downloadRemoteThirdPartyFeature(urlOrCode: string): Promise } diff --git a/packages/services/src/Domain/Session/SessionEvent.ts b/packages/services/src/Domain/Session/SessionEvent.ts index aa5ad855b..582f40c2f 100644 --- a/packages/services/src/Domain/Session/SessionEvent.ts +++ b/packages/services/src/Domain/Session/SessionEvent.ts @@ -1,5 +1,5 @@ export enum SessionEvent { - Restored = 'SessionRestored', - Revoked = 'SessionRevoked', - UserKeyPairChanged = 'UserKeyPairChanged', + Restored = 'SessionEvent:SessionRestored', + Revoked = 'SessionEvent:SessionRevoked', + UserKeyPairChanged = 'SessionEvent:UserKeyPairChanged', } diff --git a/packages/services/src/Domain/Session/SessionsClientInterface.ts b/packages/services/src/Domain/Session/SessionsClientInterface.ts index 89a3085be..f1fd5ff2b 100644 --- a/packages/services/src/Domain/Session/SessionsClientInterface.ts +++ b/packages/services/src/Domain/Session/SessionsClientInterface.ts @@ -18,8 +18,6 @@ export interface SessionsClientInterface { getWorkspaceDisplayIdentifier(): string populateSessionFromDemoShareToken(token: Base64String): Promise - initializeFromDisk(): Promise - getUser(): User | undefined isSignedIn(): boolean get userUuid(): string diff --git a/packages/services/src/Domain/Subscription/SubscriptionManager.spec.ts b/packages/services/src/Domain/Subscription/SubscriptionManager.spec.ts index 23b3411cc..ab6f960db 100644 --- a/packages/services/src/Domain/Subscription/SubscriptionManager.spec.ts +++ b/packages/services/src/Domain/Subscription/SubscriptionManager.spec.ts @@ -1,3 +1,6 @@ +import { ApplicationStage } from './../Application/ApplicationStage' +import { SessionEvent } from './../Session/SessionEvent' +import { ApplicationEvent } from './../Event/ApplicationEvent' import { StorageServiceInterface } from './../Storage/StorageServiceInterface' import { SessionsClientInterface } from './../Session/SessionsClientInterface' import { SubscriptionApiServiceInterface } from '@standardnotes/api' @@ -21,11 +24,66 @@ describe('SubscriptionManager', () => { subscriptionApiService.listInvites = jest.fn() sessions = {} as jest.Mocked + sessions.isSignedIn = jest.fn().mockReturnValue(true) storage = {} as jest.Mocked internalEventBus = {} as jest.Mocked internalEventBus.addEventHandler = jest.fn() + internalEventBus.publish = jest.fn() + }) + + describe('event handling', () => { + it('should fetch subscriptions when the application has launched', async () => { + const manager = createManager() + jest.spyOn(manager, 'fetchOnlineSubscription') + jest.spyOn(manager, 'fetchAvailableSubscriptions') + + await manager.handleEvent({ type: ApplicationEvent.Launched, payload: {} }) + + expect(manager.fetchOnlineSubscription).toHaveBeenCalledTimes(1) + expect(manager.fetchAvailableSubscriptions).toHaveBeenCalledTimes(1) + }) + + it('should fetch online subscription when user roles have changed', async () => { + const manager = createManager() + jest.spyOn(manager, 'fetchOnlineSubscription') + + await manager.handleEvent({ type: ApplicationEvent.UserRolesChanged, payload: {} }) + + expect(manager.fetchOnlineSubscription).toHaveBeenCalledTimes(1) + }) + + it('should fetch online subscription when session is restored', async () => { + const manager = createManager() + jest.spyOn(manager, 'fetchOnlineSubscription') + + await manager.handleEvent({ type: SessionEvent.Restored, payload: {} }) + + expect(manager.fetchOnlineSubscription).toHaveBeenCalledTimes(1) + }) + + it('should fetch online subscription when user has signed in', async () => { + const manager = createManager() + jest.spyOn(manager, 'fetchOnlineSubscription') + + await manager.handleEvent({ type: ApplicationEvent.SignedIn, payload: {} }) + + expect(manager.fetchOnlineSubscription).toHaveBeenCalledTimes(1) + }) + + it('should handle stage change and notify event', async () => { + const manager = createManager() + jest.spyOn(manager, 'loadSubscriptionFromStorage') + storage.getValue = jest.fn().mockReturnValue({}) + + await manager.handleEvent({ + type: ApplicationEvent.ApplicationStageChanged, + payload: { stage: ApplicationStage.StorageDecrypted_09 }, + }) + + expect(manager.loadSubscriptionFromStorage).toHaveBeenCalled() + }) }) it('should invite user by email to a shared subscription', async () => { diff --git a/packages/services/src/Domain/Subscription/SubscriptionManager.ts b/packages/services/src/Domain/Subscription/SubscriptionManager.ts index b4b1bc1ef..9d144e14f 100644 --- a/packages/services/src/Domain/Subscription/SubscriptionManager.ts +++ b/packages/services/src/Domain/Subscription/SubscriptionManager.ts @@ -1,3 +1,4 @@ +import { SessionEvent } from './../Session/SessionEvent' import { StorageKey } from './../Storage/StorageKeys' import { ApplicationStage } from './../Application/ApplicationStage' import { StorageServiceInterface } from './../Storage/StorageServiceInterface' @@ -51,6 +52,7 @@ export class SubscriptionManager } case ApplicationEvent.UserRolesChanged: + case SessionEvent.Restored: case ApplicationEvent.SignedIn: void this.fetchOnlineSubscription() break @@ -58,13 +60,17 @@ export class SubscriptionManager case ApplicationEvent.ApplicationStageChanged: { const stage = (event.payload as ApplicationStageChangedEventPayload).stage if (stage === ApplicationStage.StorageDecrypted_09) { - this.onlineSubscription = this.storage.getValue(StorageKey.Subscription) - void this.notifyEvent(SubscriptionManagerEvent.DidFetchSubscription) + this.loadSubscriptionFromStorage() } } } } + loadSubscriptionFromStorage(): void { + this.onlineSubscription = this.storage.getValue(StorageKey.Subscription) + void this.notifyEvent(SubscriptionManagerEvent.DidFetchSubscription) + } + hasOnlineSubscription(): boolean { return this.onlineSubscription != undefined } @@ -204,7 +210,7 @@ export class SubscriptionManager void this.notifyEvent(SubscriptionManagerEvent.DidFetchSubscription) } - private async fetchAvailableSubscriptions(): Promise { + async fetchAvailableSubscriptions(): Promise { try { const response = await this.subscriptionApiService.getAvailableSubscriptions() diff --git a/packages/snjs/lib/Application/Application.ts b/packages/snjs/lib/Application/Application.ts index b86cb9ccd..a94c352ca 100644 --- a/packages/snjs/lib/Application/Application.ts +++ b/packages/snjs/lib/Application/Application.ts @@ -442,12 +442,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli this.sockets.loadWebSocketUrl() - await this.sessions.initializeFromDisk() - this.settings.initializeFromDisk() - this.features.initializeFromDisk() - this.launched = true await this.notifyEvent(ApplicationEvent.Launched) await this.handleStage(ApplicationStage.Launched_10) @@ -1134,6 +1130,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli this.events.addEventHandler(this.dependencies.get(TYPES.SyncService), IntegrityEvent.IntegrityCheckCompleted) this.events.addEventHandler(this.dependencies.get(TYPES.UserService), AccountEvent.SignedInOrRegistered) this.events.addEventHandler(this.dependencies.get(TYPES.SessionManager), ApiServiceEvent.SessionRefreshed) + this.events.addEventHandler(this.dependencies.get(TYPES.SubscriptionManager), SessionEvent.Restored) this.events.addEventHandler(this.dependencies.get(TYPES.VaultInviteService), SyncEvent.ReceivedSharedVaultInvites) this.events.addEventHandler(this.dependencies.get(TYPES.VaultInviteService), SessionEvent.UserKeyPairChanged) @@ -1165,6 +1162,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli ) } + this.events.addEventHandler(this.dependencies.get(TYPES.SessionManager), ApplicationEvent.ApplicationStageChanged) this.events.addEventHandler( this.dependencies.get(TYPES.SelfContactManager), ApplicationEvent.ApplicationStageChanged, diff --git a/packages/snjs/lib/Migrations/Versions/2_42_0.ts b/packages/snjs/lib/Migrations/Versions/2_42_0.ts index af6fc44e0..be2480cad 100644 --- a/packages/snjs/lib/Migrations/Versions/2_42_0.ts +++ b/packages/snjs/lib/Migrations/Versions/2_42_0.ts @@ -1,10 +1,9 @@ import { ApplicationStage } from '@standardnotes/services' -import { FeatureIdentifier } from '@standardnotes/features' import { Migration } from '@Lib/Migrations/Migration' import { ThemeInterface } from '@standardnotes/models' import { ContentType } from '@standardnotes/domain-core' -const NoDistractionIdentifier = 'org.standardnotes.theme-no-distraction' as FeatureIdentifier +const NoDistractionIdentifier = 'org.standardnotes.theme-no-distraction' export class Migration2_42_0 extends Migration { static override version(): string { diff --git a/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts b/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts index 67e17731d..fec2bda05 100644 --- a/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts +++ b/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts @@ -1,5 +1,5 @@ import { FeaturesService } from '@Lib/Services/Features/FeaturesService' -import { ContentType } from '@standardnotes/domain-core' +import { ContentType, Uuid } from '@standardnotes/domain-core' import { ActionObserver, PayloadEmitSource, @@ -10,7 +10,6 @@ import { UIFeature, ComponentInterface, PrefKey, - ThemeInterface, ComponentPreferencesEntry, AllComponentPreferences, SNNote, @@ -21,15 +20,14 @@ import { import { ComponentArea, FindNativeFeature, - FeatureIdentifier, EditorFeatureDescription, FindNativeTheme, IframeComponentFeatureDescription, ComponentFeatureDescription, ThemeFeatureDescription, - EditorIdentifier, GetIframeEditors, GetNativeThemes, + NativeFeatureIdentifier, } from '@standardnotes/features' import { Copy, removeFromArray, sleep, isNotUndefined } from '@standardnotes/utils' import { ComponentViewer } from '@Lib/Services/ComponentManager/ComponentViewer' @@ -287,7 +285,7 @@ export class SNComponentManager const url = this.urlForFeature(feature) if (url) { - this.device.registerComponentUrl(feature.uniqueIdentifier, url) + this.device.registerComponentUrl(feature.uniqueIdentifier.value, url) } } } @@ -372,7 +370,7 @@ export class SNComponentManager return } - const featureStatus = this.features.getFeatureStatus(uiFeature.featureIdentifier) + const featureStatus = this.features.getFeatureStatus(uiFeature.uniqueIdentifier) if (featureStatus !== FeatureStatus.Entitled) { return } @@ -398,28 +396,50 @@ export class SNComponentManager } public getActiveThemes(): UIFeature[] { - const activeThemesIdentifiers = this.getActiveThemesIdentifiers() + const { features, uuids } = this.getActiveThemesIdentifiers() - const thirdPartyThemes = this.items.findItems(activeThemesIdentifiers).map((item) => { - return new UIFeature(item) - }) + const thirdPartyThemes = uuids + .map((uuid) => { + const component = this.items.findItem(uuid.value) + if (component) { + return new UIFeature(component) + } + return undefined + }) + .filter(isNotUndefined) - const nativeThemes = activeThemesIdentifiers + const nativeThemes = features .map((identifier) => { - return FindNativeTheme(identifier as FeatureIdentifier) + return FindNativeTheme(identifier.value) }) .filter(isNotUndefined) .map((theme) => new UIFeature(theme)) const entitledThemes = [...thirdPartyThemes, ...nativeThemes].filter((theme) => { - return this.features.getFeatureStatus(theme.featureIdentifier) === FeatureStatus.Entitled + return this.features.getFeatureStatus(theme.uniqueIdentifier) === FeatureStatus.Entitled }) return entitledThemes } - public getActiveThemesIdentifiers(): string[] { - return this.preferences.getValue(PrefKey.ActiveThemes, undefined) ?? [] + public getActiveThemesIdentifiers(): { features: NativeFeatureIdentifier[]; uuids: Uuid[] } { + const features: NativeFeatureIdentifier[] = [] + const uuids: Uuid[] = [] + + const strings = this.preferences.getValue(PrefKey.ActiveThemes, undefined) ?? [] + for (const string of strings) { + const nativeIdentifier = NativeFeatureIdentifier.create(string) + if (!nativeIdentifier.isFailed()) { + features.push(nativeIdentifier.getValue()) + } + + const uuid = Uuid.create(string) + if (!uuid.isFailed()) { + uuids.push(uuid.getValue()) + } + } + + return { features, uuids } } public async toggleComponent(component: ComponentInterface): Promise { @@ -437,7 +457,7 @@ export class SNComponentManager return usecase.execute(note) } - getDefaultEditorIdentifier(currentTag?: SNTag): EditorIdentifier { + getDefaultEditorIdentifier(currentTag?: SNTag): string { const usecase = new GetDefaultEditorIdentifier(this.preferences, this.items) return usecase.execute(currentTag).getValue() } @@ -468,7 +488,7 @@ export class SNComponentManager this.preferences.getValue(PrefKey.ComponentPreferences, undefined) ?? {}, ) - const preferencesLookupKey = uiFeature.uniqueIdentifier + const preferencesLookupKey = uiFeature.uniqueIdentifier.value mutablePreferencesValue[preferencesLookupKey] = preferences @@ -482,7 +502,7 @@ export class SNComponentManager return undefined } - const preferencesLookupKey = component.uniqueIdentifier + const preferencesLookupKey = component.uniqueIdentifier.value return preferences[preferencesLookupKey] } @@ -490,31 +510,31 @@ export class SNComponentManager async addActiveTheme(theme: UIFeature): Promise { const activeThemes = (this.preferences.getValue(PrefKey.ActiveThemes, undefined) ?? []).slice() - activeThemes.push(theme.uniqueIdentifier) + activeThemes.push(theme.uniqueIdentifier.value) await this.preferences.setValue(PrefKey.ActiveThemes, activeThemes) } async replaceActiveTheme(theme: UIFeature): Promise { - await this.preferences.setValue(PrefKey.ActiveThemes, [theme.uniqueIdentifier]) + await this.preferences.setValue(PrefKey.ActiveThemes, [theme.uniqueIdentifier.value]) } async removeActiveTheme(theme: UIFeature): Promise { const activeThemes = this.preferences.getValue(PrefKey.ActiveThemes, undefined) ?? [] - const filteredThemes = activeThemes.filter((activeTheme) => activeTheme !== theme.uniqueIdentifier) + const filteredThemes = activeThemes.filter((activeTheme) => activeTheme !== theme.uniqueIdentifier.value) await this.preferences.setValue(PrefKey.ActiveThemes, filteredThemes) } isThemeActive(theme: UIFeature): boolean { - if (this.features.getFeatureStatus(theme.featureIdentifier) !== FeatureStatus.Entitled) { + if (this.features.getFeatureStatus(theme.uniqueIdentifier) !== FeatureStatus.Entitled) { return false } const activeThemes = this.preferences.getValue(PrefKey.ActiveThemes, undefined) ?? [] - return activeThemes.includes(theme.uniqueIdentifier) + return activeThemes.includes(theme.uniqueIdentifier.value) } async addActiveComponent(component: ComponentInterface): Promise { diff --git a/packages/snjs/lib/Services/ComponentManager/ComponentViewer.ts b/packages/snjs/lib/Services/ComponentManager/ComponentViewer.ts index 098eb8c71..d3ea572fb 100644 --- a/packages/snjs/lib/Services/ComponentManager/ComponentViewer.ts +++ b/packages/snjs/lib/Services/ComponentManager/ComponentViewer.ts @@ -58,6 +58,7 @@ import { ComponentPermission, ComponentArea, IframeComponentFeatureDescription, + NativeFeatureIdentifier, } from '@standardnotes/features' import { isString, @@ -72,7 +73,7 @@ import { isNotUndefined, uniqueArray, } from '@standardnotes/utils' -import { ContentType } from '@standardnotes/domain-core' +import { ContentType, Uuid } from '@standardnotes/domain-core' export class ComponentViewer implements ComponentViewerInterface { private streamItems?: string[] @@ -214,12 +215,12 @@ export class ComponentViewer implements ComponentViewerInterface { this.readonly = readonly } - get componentUniqueIdentifier(): string { + get componentUniqueIdentifier(): NativeFeatureIdentifier | Uuid { return this.componentOrFeature.uniqueIdentifier } public getFeatureStatus(): FeatureStatus { - return this.services.features.getFeatureStatus(this.componentOrFeature.featureIdentifier, { + return this.services.features.getFeatureStatus(this.componentUniqueIdentifier, { inContextOfItem: this.getContextItem(), }) } @@ -269,7 +270,9 @@ export class ComponentViewer implements ComponentViewerInterface { return } - const updatedComponent = items.find((item) => item.uuid === this.componentUniqueIdentifier) as ComponentInterface + const updatedComponent = items.find( + (item) => item.uuid === this.componentUniqueIdentifier.value, + ) as ComponentInterface if (!updatedComponent) { return } @@ -289,7 +292,7 @@ export class ComponentViewer implements ComponentViewerInterface { this.updateOurComponentRefFromChangedItems(nondeletedItems) - const areWeOriginator = sourceKey && sourceKey === this.componentUniqueIdentifier + const areWeOriginator = sourceKey && sourceKey === this.componentUniqueIdentifier.value if (areWeOriginator) { return } @@ -326,7 +329,7 @@ export class ComponentViewer implements ComponentViewerInterface { ] this.config.componentManagerFunctions.runWithPermissionsUseCase.execute( - this.componentUniqueIdentifier, + this.componentUniqueIdentifier.value, requiredPermissions, () => { this.sendItemsInReply(items, this.streamItemsOriginalMessage as ComponentMessage) @@ -341,7 +344,7 @@ export class ComponentViewer implements ComponentViewerInterface { }, ] as ComponentPermission[] this.config.componentManagerFunctions.runWithPermissionsUseCase.execute( - this.componentUniqueIdentifier, + this.componentUniqueIdentifier.value, requiredContextPermissions, () => { this.log( @@ -414,7 +417,7 @@ export class ComponentViewer implements ComponentViewerInterface { private getClientData(item: DecryptedItemInterface): Record { const globalComponentData = item.getDomainData(ComponentDataDomain) || {} - const thisComponentData = globalComponentData[this.componentUniqueIdentifier] || {} + const thisComponentData = globalComponentData[this.componentUniqueIdentifier.value] || {} return thisComponentData as Record } @@ -530,7 +533,7 @@ export class ComponentViewer implements ComponentViewerInterface { sessionKey: this.sessionKey, componentData: componentData, data: { - uuid: this.componentUniqueIdentifier, + uuid: this.componentUniqueIdentifier.value, environment: environmentToString(this.config.environment), platform: platformToString(this.config.platform), activeThemeUrls: this.config.componentManagerFunctions.urlsForActiveThemes(), @@ -602,7 +605,7 @@ export class ComponentViewer implements ComponentViewerInterface { }, ] this.config.componentManagerFunctions.runWithPermissionsUseCase.execute( - this.componentUniqueIdentifier, + this.componentUniqueIdentifier.value, requiredPermissions, () => { if (!this.streamItems) { @@ -627,7 +630,7 @@ export class ComponentViewer implements ComponentViewerInterface { ] this.config.componentManagerFunctions.runWithPermissionsUseCase.execute( - this.componentUniqueIdentifier, + this.componentUniqueIdentifier.value, requiredPermissions, () => { if (!this.streamContextItemOriginalMessage) { @@ -684,7 +687,7 @@ export class ComponentViewer implements ComponentViewerInterface { } this.config.componentManagerFunctions.runWithPermissionsUseCase.execute( - this.componentUniqueIdentifier, + this.componentUniqueIdentifier.value, requiredPermissions, async () => { @@ -766,13 +769,13 @@ export class ComponentViewer implements ComponentViewerInterface { const allComponentData = Copy>( mutator.getItem().getDomainData(ComponentDataDomain) || {}, ) - allComponentData[this.componentUniqueIdentifier] = responseItem.clientData + allComponentData[this.componentUniqueIdentifier.value] = responseItem.clientData mutator.setDomainData(allComponentData, ComponentDataDomain) } }, MutationType.UpdateUserTimestamps, PayloadEmitSource.ComponentRetrieved, - this.componentUniqueIdentifier, + this.componentUniqueIdentifier.value, ) this.services.sync @@ -807,7 +810,7 @@ export class ComponentViewer implements ComponentViewerInterface { ] this.config.componentManagerFunctions.runWithPermissionsUseCase.execute( - this.componentUniqueIdentifier, + this.componentUniqueIdentifier.value, requiredPermissions, async () => { responseItems = this.responseItemsByRemovingPrivateProperties(responseItems) @@ -834,13 +837,13 @@ export class ComponentViewer implements ComponentViewerInterface { const allComponentClientData = Copy>( item.getDomainData(ComponentDataDomain) || {}, ) - allComponentClientData[this.componentUniqueIdentifier] = responseItem.clientData + allComponentClientData[this.componentUniqueIdentifier.value] = responseItem.clientData mutator.setDomainData(allComponentClientData, ComponentDataDomain) } }, MutationType.UpdateUserTimestamps, PayloadEmitSource.ComponentCreated, - this.componentUniqueIdentifier, + this.componentUniqueIdentifier.value, ) processedItems.push(item) } @@ -874,7 +877,7 @@ export class ComponentViewer implements ComponentViewerInterface { ] this.config.componentManagerFunctions.runWithPermissionsUseCase.execute( - this.componentUniqueIdentifier, + this.componentUniqueIdentifier.value, requiredPermissions, async () => { const itemsData = items @@ -911,7 +914,7 @@ export class ComponentViewer implements ComponentViewerInterface { private handleSetComponentPreferencesMessage(message: ComponentMessage): void { const noPermissionsRequired: ComponentPermission[] = [] this.config.componentManagerFunctions.runWithPermissionsUseCase.execute( - this.componentUniqueIdentifier, + this.componentUniqueIdentifier.value, noPermissionsRequired, async () => { const newPreferences = message.data.componentData diff --git a/packages/snjs/lib/Services/ComponentManager/Types.ts b/packages/snjs/lib/Services/ComponentManager/Types.ts index 7ea63bbca..1bca86b7f 100644 --- a/packages/snjs/lib/Services/ComponentManager/Types.ts +++ b/packages/snjs/lib/Services/ComponentManager/Types.ts @@ -1,4 +1,4 @@ -import { ComponentArea, ComponentAction, FeatureIdentifier, LegacyFileSafeIdentifier } from '@standardnotes/features' +import { ComponentArea, ComponentAction, NativeFeatureIdentifier } from '@standardnotes/features' import { ComponentMessage, MessageData, OutgoingItemMessagePayload } from '@standardnotes/models' import { UuidString } from '@Lib/Types/UuidString' import { ContentType } from '@standardnotes/domain-core' @@ -17,9 +17,9 @@ export type Writeable = { -readonly [P in keyof T]: T[P] } * Extensions allowed to batch stream AllowedBatchContentTypes */ export const AllowedBatchStreaming = Object.freeze([ - LegacyFileSafeIdentifier, - FeatureIdentifier.DeprecatedFileSafe, - FeatureIdentifier.DeprecatedBoldEditor, + NativeFeatureIdentifier.TYPES.LegacyFileSafeIdentifier, + NativeFeatureIdentifier.TYPES.DeprecatedFileSafe, + NativeFeatureIdentifier.TYPES.DeprecatedBoldEditor, ]) /** diff --git a/packages/snjs/lib/Services/ComponentManager/UseCase/DoesEditorChangeRequireAlert.spec.ts b/packages/snjs/lib/Services/ComponentManager/UseCase/DoesEditorChangeRequireAlert.spec.ts index 42917f91b..4f15fd94f 100644 --- a/packages/snjs/lib/Services/ComponentManager/UseCase/DoesEditorChangeRequireAlert.spec.ts +++ b/packages/snjs/lib/Services/ComponentManager/UseCase/DoesEditorChangeRequireAlert.spec.ts @@ -1,5 +1,5 @@ import { - FeatureIdentifier, + NativeFeatureIdentifier, FindNativeFeature, IframeComponentFeatureDescription, UIFeatureDescriptionTypes, @@ -7,7 +7,7 @@ import { import { DoesEditorChangeRequireAlertUseCase } from './DoesEditorChangeRequireAlert' import { UIFeature } from '@standardnotes/models' -const nativeFeatureAsUIFeature = (identifier: FeatureIdentifier) => { +const nativeFeatureAsUIFeature = (identifier: string) => { return new UIFeature(FindNativeFeature(identifier)!) } @@ -19,57 +19,77 @@ describe('editor change alert', () => { }) it('should not require alert switching from plain editor', () => { - const component = nativeFeatureAsUIFeature(FeatureIdentifier.MarkdownProEditor)! + const component = nativeFeatureAsUIFeature( + NativeFeatureIdentifier.TYPES.MarkdownProEditor, + )! const requiresAlert = usecase.execute(undefined, component) expect(requiresAlert).toBe(false) }) it('should not require alert switching to plain editor', () => { - const component = nativeFeatureAsUIFeature(FeatureIdentifier.MarkdownProEditor)! + const component = nativeFeatureAsUIFeature( + NativeFeatureIdentifier.TYPES.MarkdownProEditor, + )! const requiresAlert = usecase.execute(component, undefined) expect(requiresAlert).toBe(false) }) it('should not require alert switching from a markdown editor', () => { - const htmlEditor = nativeFeatureAsUIFeature(FeatureIdentifier.PlusEditor)! + const htmlEditor = nativeFeatureAsUIFeature( + NativeFeatureIdentifier.TYPES.PlusEditor, + )! const markdownEditor = nativeFeatureAsUIFeature( - FeatureIdentifier.MarkdownProEditor, + NativeFeatureIdentifier.TYPES.MarkdownProEditor, ) const requiresAlert = usecase.execute(markdownEditor, htmlEditor) expect(requiresAlert).toBe(false) }) it('should not require alert switching to a markdown editor', () => { - const htmlEditor = nativeFeatureAsUIFeature(FeatureIdentifier.PlusEditor)! + const htmlEditor = nativeFeatureAsUIFeature( + NativeFeatureIdentifier.TYPES.PlusEditor, + )! const markdownEditor = nativeFeatureAsUIFeature( - FeatureIdentifier.MarkdownProEditor, + NativeFeatureIdentifier.TYPES.MarkdownProEditor, ) const requiresAlert = usecase.execute(htmlEditor, markdownEditor) expect(requiresAlert).toBe(false) }) it('should not require alert switching from & to a html editor', () => { - const htmlEditor = nativeFeatureAsUIFeature(FeatureIdentifier.PlusEditor)! + const htmlEditor = nativeFeatureAsUIFeature( + NativeFeatureIdentifier.TYPES.PlusEditor, + )! const requiresAlert = usecase.execute(htmlEditor, htmlEditor) expect(requiresAlert).toBe(false) }) it('should require alert switching from a html editor to custom editor', () => { - const htmlEditor = nativeFeatureAsUIFeature(FeatureIdentifier.PlusEditor)! - const customEditor = nativeFeatureAsUIFeature(FeatureIdentifier.TokenVaultEditor) + const htmlEditor = nativeFeatureAsUIFeature( + NativeFeatureIdentifier.TYPES.PlusEditor, + )! + const customEditor = nativeFeatureAsUIFeature( + NativeFeatureIdentifier.TYPES.TokenVaultEditor, + ) const requiresAlert = usecase.execute(htmlEditor, customEditor) expect(requiresAlert).toBe(true) }) it('should require alert switching from a custom editor to html editor', () => { - const htmlEditor = nativeFeatureAsUIFeature(FeatureIdentifier.PlusEditor)! - const customEditor = nativeFeatureAsUIFeature(FeatureIdentifier.TokenVaultEditor) + const htmlEditor = nativeFeatureAsUIFeature( + NativeFeatureIdentifier.TYPES.PlusEditor, + )! + const customEditor = nativeFeatureAsUIFeature( + NativeFeatureIdentifier.TYPES.TokenVaultEditor, + ) const requiresAlert = usecase.execute(customEditor, htmlEditor) expect(requiresAlert).toBe(true) }) it('should require alert switching from a custom editor to custom editor', () => { - const customEditor = nativeFeatureAsUIFeature(FeatureIdentifier.TokenVaultEditor) + const customEditor = nativeFeatureAsUIFeature( + NativeFeatureIdentifier.TYPES.TokenVaultEditor, + ) const requiresAlert = usecase.execute(customEditor, customEditor) expect(requiresAlert).toBe(true) }) diff --git a/packages/snjs/lib/Services/ComponentManager/UseCase/EditorForNote.spec.ts b/packages/snjs/lib/Services/ComponentManager/UseCase/EditorForNote.spec.ts index 1652b33da..729a61fd8 100644 --- a/packages/snjs/lib/Services/ComponentManager/UseCase/EditorForNote.spec.ts +++ b/packages/snjs/lib/Services/ComponentManager/UseCase/EditorForNote.spec.ts @@ -1,5 +1,5 @@ import { createNote } from '@Lib/Spec/SpecUtils' -import { FeatureIdentifier, NoteType } from '@standardnotes/features' +import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features' import { EditorForNoteUseCase } from './EditorForNote' import { ItemManagerInterface } from '@standardnotes/services' @@ -17,7 +17,7 @@ describe('EditorForNote', () => { noteType: NoteType.Plain, }) - expect(usecase.execute(note).featureIdentifier).toBe(FeatureIdentifier.PlainEditor) + expect(usecase.execute(note).featureIdentifier).toBe(NativeFeatureIdentifier.TYPES.PlainEditor) }) it('getEditorForNote should call legacy function if no note editorIdentifier or noteType', () => { diff --git a/packages/snjs/lib/Services/ComponentManager/UseCase/EditorForNote.ts b/packages/snjs/lib/Services/ComponentManager/UseCase/EditorForNote.ts index d99a0e5a4..9020b6221 100644 --- a/packages/snjs/lib/Services/ComponentManager/UseCase/EditorForNote.ts +++ b/packages/snjs/lib/Services/ComponentManager/UseCase/EditorForNote.ts @@ -1,7 +1,6 @@ import { ComponentArea, EditorFeatureDescription, - FeatureIdentifier, FindNativeFeature, GetIframeAndNativeEditors, GetPlainNoteFeature, @@ -47,11 +46,9 @@ export class EditorForNoteUseCase { } private componentOrNativeFeatureForIdentifier( - identifier: FeatureIdentifier | string, + identifier: string, ): UIFeature | undefined { - const nativeFeature = FindNativeFeature( - identifier as FeatureIdentifier, - ) + const nativeFeature = FindNativeFeature(identifier) if (nativeFeature) { return new UIFeature(nativeFeature) } diff --git a/packages/snjs/lib/Services/ComponentManager/UseCase/GetDefaultEditorIdentifier.spec.ts b/packages/snjs/lib/Services/ComponentManager/UseCase/GetDefaultEditorIdentifier.spec.ts index 81b955479..f6e2276d6 100644 --- a/packages/snjs/lib/Services/ComponentManager/UseCase/GetDefaultEditorIdentifier.spec.ts +++ b/packages/snjs/lib/Services/ComponentManager/UseCase/GetDefaultEditorIdentifier.spec.ts @@ -1,6 +1,6 @@ import { ItemManagerInterface, PreferenceServiceInterface } from '@standardnotes/services' import { GetDefaultEditorIdentifier } from './GetDefaultEditorIdentifier' -import { ComponentArea, FeatureIdentifier } from '@standardnotes/features' +import { ComponentArea, NativeFeatureIdentifier } from '@standardnotes/features' import { SNComponent, SNTag } from '@standardnotes/models' describe('getDefaultEditorIdentifier', () => { @@ -21,33 +21,33 @@ describe('getDefaultEditorIdentifier', () => { it('should return plain editor if no default tag editor or component editor', () => { const editorIdentifier = usecase.execute().getValue() - expect(editorIdentifier).toEqual(FeatureIdentifier.PlainEditor) + expect(editorIdentifier).toEqual(NativeFeatureIdentifier.TYPES.PlainEditor) }) it('should return pref key based value if available', () => { - preferences.getValue = jest.fn().mockReturnValue(FeatureIdentifier.SuperEditor) + preferences.getValue = jest.fn().mockReturnValue(NativeFeatureIdentifier.TYPES.SuperEditor) const editorIdentifier = usecase.execute().getValue() - expect(editorIdentifier).toEqual(FeatureIdentifier.SuperEditor) + expect(editorIdentifier).toEqual(NativeFeatureIdentifier.TYPES.SuperEditor) }) it('should return default tag identifier if tag supplied', () => { const tag = { preferences: { - editorIdentifier: FeatureIdentifier.SuperEditor, + editorIdentifier: NativeFeatureIdentifier.TYPES.SuperEditor, }, } as jest.Mocked const editorIdentifier = usecase.execute(tag).getValue() - expect(editorIdentifier).toEqual(FeatureIdentifier.SuperEditor) + expect(editorIdentifier).toEqual(NativeFeatureIdentifier.TYPES.SuperEditor) }) it('should return legacy editor identifier', () => { const editor = { legacyIsDefaultEditor: jest.fn().mockReturnValue(true), - identifier: FeatureIdentifier.MarkdownProEditor, + identifier: NativeFeatureIdentifier.TYPES.MarkdownProEditor, area: ComponentArea.Editor, } as unknown as jest.Mocked @@ -55,6 +55,6 @@ describe('getDefaultEditorIdentifier', () => { const editorIdentifier = usecase.execute().getValue() - expect(editorIdentifier).toEqual(FeatureIdentifier.MarkdownProEditor) + expect(editorIdentifier).toEqual(NativeFeatureIdentifier.TYPES.MarkdownProEditor) }) }) diff --git a/packages/snjs/lib/Services/ComponentManager/UseCase/GetDefaultEditorIdentifier.ts b/packages/snjs/lib/Services/ComponentManager/UseCase/GetDefaultEditorIdentifier.ts index 9021f6f47..56d5642b9 100644 --- a/packages/snjs/lib/Services/ComponentManager/UseCase/GetDefaultEditorIdentifier.ts +++ b/packages/snjs/lib/Services/ComponentManager/UseCase/GetDefaultEditorIdentifier.ts @@ -1,12 +1,12 @@ import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core' -import { ComponentArea, EditorIdentifier, FeatureIdentifier } from '@standardnotes/features' +import { ComponentArea, NativeFeatureIdentifier } from '@standardnotes/features' import { ComponentInterface, PrefKey, SNTag } from '@standardnotes/models' import { ItemManagerInterface, PreferenceServiceInterface } from '@standardnotes/services' -export class GetDefaultEditorIdentifier implements SyncUseCaseInterface { +export class GetDefaultEditorIdentifier implements SyncUseCaseInterface { constructor(private preferences: PreferenceServiceInterface, private items: ItemManagerInterface) {} - execute(currentTag?: SNTag): Result { + execute(currentTag?: SNTag): Result { if (currentTag) { const editorIdentifier = currentTag?.preferences?.editorIdentifier if (editorIdentifier) { @@ -25,7 +25,7 @@ export class GetDefaultEditorIdentifier implements SyncUseCaseInterface(identifier: FeatureIdentifier) => { +const nativeFeatureAsUIFeature = (identifier: string) => { return new UIFeature(FindNativeFeature(identifier)!) } @@ -35,7 +35,7 @@ const thirdPartyFeature = () => { local_url: 'sn://Extensions/non-native-identifier/dist/index.html', hosted_url: 'https://example.com/component', package_info: { - identifier: 'non-native-identifier' as FeatureIdentifier, + identifier: 'non-native-identifier', expires_at: new Date().getTime(), availableInRoles: [], } as unknown as jest.Mocked, @@ -75,7 +75,9 @@ describe('GetFeatureUrl', () => { }) it('returns native path for native component', () => { - const feature = nativeFeatureAsUIFeature(FeatureIdentifier.MarkdownProEditor)! + const feature = nativeFeatureAsUIFeature( + NativeFeatureIdentifier.TYPES.MarkdownProEditor, + )! const url = usecase.execute(feature) expect(url).toEqual( `${desktopExtHost}/components/${feature.featureIdentifier}/${feature.asFeatureDescription.index_path}`, @@ -84,7 +86,7 @@ describe('GetFeatureUrl', () => { it('returns native path for deprecated native component', () => { const feature = nativeFeatureAsUIFeature( - FeatureIdentifier.DeprecatedBoldEditor, + NativeFeatureIdentifier.TYPES.DeprecatedBoldEditor, )! const url = usecase.execute(feature) expect(url).toEqual( @@ -122,7 +124,9 @@ describe('GetFeatureUrl', () => { }) it('returns native path for native feature', () => { - const feature = nativeFeatureAsUIFeature(FeatureIdentifier.MarkdownProEditor) + const feature = nativeFeatureAsUIFeature( + NativeFeatureIdentifier.TYPES.MarkdownProEditor, + ) const url = usecase.execute(feature) expect(url).toEqual( `http://localhost/components/assets/${feature.featureIdentifier}/${feature.asFeatureDescription.index_path}`, diff --git a/packages/snjs/lib/Services/ComponentManager/UseCase/RunWithPermissionsUseCase.spec.ts b/packages/snjs/lib/Services/ComponentManager/UseCase/RunWithPermissionsUseCase.spec.ts index cf0882994..3e2cd4168 100644 --- a/packages/snjs/lib/Services/ComponentManager/UseCase/RunWithPermissionsUseCase.spec.ts +++ b/packages/snjs/lib/Services/ComponentManager/UseCase/RunWithPermissionsUseCase.spec.ts @@ -2,7 +2,7 @@ import { ContentType } from '@standardnotes/domain-core' import { ComponentAction, ComponentPermission, - FeatureIdentifier, + NativeFeatureIdentifier, FindNativeFeature, UIFeatureDescriptionTypes, } from '@standardnotes/features' @@ -15,7 +15,7 @@ import { SyncServiceInterface, } from '@standardnotes/services' -const nativeFeatureAsUIFeature = (identifier: FeatureIdentifier) => { +const nativeFeatureAsUIFeature = (identifier: string) => { return new UIFeature(FindNativeFeature(identifier)!) } @@ -43,7 +43,7 @@ describe('RunWithPermissionsUseCase', () => { expect( usecase.areRequestedPermissionsValid( - nativeFeatureAsUIFeature(FeatureIdentifier.MarkdownProEditor), + nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.MarkdownProEditor), permissions, ), ).toEqual(true) @@ -59,7 +59,7 @@ describe('RunWithPermissionsUseCase', () => { expect( usecase.areRequestedPermissionsValid( - nativeFeatureAsUIFeature(FeatureIdentifier.MarkdownProEditor), + nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.MarkdownProEditor), permissions, ), ).toEqual(false) @@ -75,7 +75,7 @@ describe('RunWithPermissionsUseCase', () => { expect( usecase.areRequestedPermissionsValid( - nativeFeatureAsUIFeature(FeatureIdentifier.MarkdownProEditor), + nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.MarkdownProEditor), permissions, ), ).toEqual(false) @@ -91,7 +91,7 @@ describe('RunWithPermissionsUseCase', () => { expect( usecase.areRequestedPermissionsValid( - nativeFeatureAsUIFeature(FeatureIdentifier.MarkdownProEditor), + nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.MarkdownProEditor), permissions, ), ).toEqual(false) @@ -107,7 +107,7 @@ describe('RunWithPermissionsUseCase', () => { expect( usecase.areRequestedPermissionsValid( - nativeFeatureAsUIFeature(FeatureIdentifier.DeprecatedFileSafe), + nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.DeprecatedFileSafe), permissions, ), ).toEqual(false) @@ -127,7 +127,7 @@ describe('RunWithPermissionsUseCase', () => { expect( usecase.areRequestedPermissionsValid( - nativeFeatureAsUIFeature(FeatureIdentifier.DeprecatedFileSafe), + nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.DeprecatedFileSafe), permissions, ), ).toEqual(true) @@ -147,7 +147,7 @@ describe('RunWithPermissionsUseCase', () => { expect( usecase.areRequestedPermissionsValid( - nativeFeatureAsUIFeature(FeatureIdentifier.DeprecatedBoldEditor), + nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.DeprecatedBoldEditor), permissions, ), ).toEqual(true) @@ -166,7 +166,10 @@ describe('RunWithPermissionsUseCase', () => { ] expect( - usecase.areRequestedPermissionsValid(nativeFeatureAsUIFeature(FeatureIdentifier.PlusEditor), permissions), + usecase.areRequestedPermissionsValid( + nativeFeatureAsUIFeature(NativeFeatureIdentifier.TYPES.PlusEditor), + permissions, + ), ).toEqual(false) }) }) diff --git a/packages/snjs/lib/Services/ComponentManager/UseCase/RunWithPermissionsUseCase.ts b/packages/snjs/lib/Services/ComponentManager/UseCase/RunWithPermissionsUseCase.ts index decbbe55e..5e11fc08d 100644 --- a/packages/snjs/lib/Services/ComponentManager/UseCase/RunWithPermissionsUseCase.ts +++ b/packages/snjs/lib/Services/ComponentManager/UseCase/RunWithPermissionsUseCase.ts @@ -2,7 +2,6 @@ import { ComponentAction, ComponentFeatureDescription, ComponentPermission, - FeatureIdentifier, FindNativeFeature, } from '@standardnotes/features' import { ComponentInterface, ComponentMutator, PermissionDialog, UIFeature } from '@standardnotes/models' @@ -228,7 +227,7 @@ export class RunWithPermissionsUseCase { } private findUIFeature(identifier: string): UIFeature | undefined { - const nativeFeature = FindNativeFeature(identifier as FeatureIdentifier) + const nativeFeature = FindNativeFeature(identifier) if (nativeFeature) { return new UIFeature(nativeFeature) } diff --git a/packages/snjs/lib/Services/Features/FeaturesService.spec.ts b/packages/snjs/lib/Services/Features/FeaturesService.spec.ts index dfc579672..85ff6655e 100644 --- a/packages/snjs/lib/Services/Features/FeaturesService.spec.ts +++ b/packages/snjs/lib/Services/Features/FeaturesService.spec.ts @@ -2,8 +2,8 @@ import { ItemInterface, SNFeatureRepo } from '@standardnotes/models' import { SyncService } from '../Sync/SyncService' import { SettingName } from '@standardnotes/settings' import { FeaturesService } from '@Lib/Services/Features' -import { RoleName, ContentType } from '@standardnotes/domain-core' -import { FeatureIdentifier, GetFeatures } from '@standardnotes/features' +import { RoleName, ContentType, Uuid } from '@standardnotes/domain-core' +import { NativeFeatureIdentifier, GetFeatures } from '@standardnotes/features' import { WebSocketsService } from '../Api/WebsocketsService' import { SettingsService } from '../Settings' import { PureCryptoInterface } from '@standardnotes/sncrypto-common' @@ -44,6 +44,7 @@ describe('FeaturesService', () => { let roles: string[] let items: ItemInterface[] let internalEventBus: InternalEventBusInterface + let featureService: FeaturesService const createService = () => { return new FeaturesService( @@ -118,23 +119,81 @@ describe('FeaturesService', () => { internalEventBus = {} as jest.Mocked internalEventBus.publish = jest.fn() internalEventBus.addEventHandler = jest.fn() + + featureService = new FeaturesService( + storageService, + itemManager, + mutator, + subscriptions, + apiService, + webSocketsService, + settingsService, + userService, + syncService, + alertService, + sessionManager, + crypto, + internalEventBus, + ) }) describe('experimental features', () => { it('enables/disables an experimental feature', async () => { storageService.getValue = jest.fn().mockReturnValue(GetFeatures()) - const featuresService = createService() - featuresService.getExperimentalFeatures = jest.fn().mockReturnValue([FeatureIdentifier.PlusEditor]) - featuresService.initializeFromDisk() + featureService.getExperimentalFeatures = jest.fn().mockReturnValue([NativeFeatureIdentifier.TYPES.PlusEditor]) + featureService.initializeFromDisk() - featuresService.enableExperimentalFeature(FeatureIdentifier.PlusEditor) + featureService.enableExperimentalFeature(NativeFeatureIdentifier.TYPES.PlusEditor) - expect(featuresService.isExperimentalFeatureEnabled(FeatureIdentifier.PlusEditor)).toEqual(true) + expect(featureService.isExperimentalFeatureEnabled(NativeFeatureIdentifier.TYPES.PlusEditor)).toEqual(true) - featuresService.disableExperimentalFeature(FeatureIdentifier.PlusEditor) + featureService.disableExperimentalFeature(NativeFeatureIdentifier.TYPES.PlusEditor) - expect(featuresService.isExperimentalFeatureEnabled(FeatureIdentifier.PlusEditor)).toEqual(false) + expect(featureService.isExperimentalFeatureEnabled(NativeFeatureIdentifier.TYPES.PlusEditor)).toEqual(false) + }) + }) + + describe('hasFirstPartyOnlineSubscription', () => { + it('should be true if signed into first party server and has online subscription', () => { + sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true) + subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true) + + expect(featureService.hasFirstPartyOnlineSubscription()).toEqual(true) + }) + + it('should not be true if not signed into first party server', () => { + sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false) + subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true) + + expect(featureService.hasFirstPartyOnlineSubscription()).toEqual(false) + }) + + it('should not be true if no online subscription', () => { + sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true) + subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(false) + + expect(featureService.hasFirstPartyOnlineSubscription()).toEqual(false) + }) + }) + + describe('hasPaidAnyPartyOnlineOrOfflineSubscription', () => { + it('should return true if onlineRolesIncludePaidSubscription', () => { + featureService.onlineRolesIncludePaidSubscription = jest.fn().mockReturnValue(true) + + expect(featureService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true) + }) + + it('should return true if hasOfflineRepo', () => { + featureService.hasOfflineRepo = jest.fn().mockReturnValue(true) + + expect(featureService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true) + }) + + it('should return true if hasFirstPartyOnlineSubscription', () => { + featureService.hasFirstPartyOnlineSubscription = jest.fn().mockReturnValue(true) + + expect(featureService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true) }) }) @@ -148,40 +207,40 @@ describe('FeaturesService', () => { describe('updateRoles()', () => { it('setRoles should notify event if roles changed', async () => { storageService.getValue = jest.fn().mockReturnValue(roles) - const featuresService = createService() - featuresService.initializeFromDisk() - const mock = (featuresService['notifyEvent'] = jest.fn()) + featureService.initializeFromDisk() + + const mock = (featureService['notifyEvent'] = jest.fn()) const newRoles = [...roles, RoleName.NAMES.PlusUser] - featuresService.setOnlineRoles(newRoles) + featureService.setOnlineRoles(newRoles) expect(mock.mock.calls[0][0]).toEqual(FeaturesEvent.UserRolesChanged) }) it('should notify of subscription purchase', async () => { storageService.getValue = jest.fn().mockReturnValue(roles) - const featuresService = createService() - featuresService.initializeFromDisk() - const spy = jest.spyOn(featuresService, 'notifyEvent' as never) + featureService.initializeFromDisk() + + const spy = jest.spyOn(featureService, 'notifyEvent' as never) const newRoles = [...roles, RoleName.NAMES.ProUser] - await featuresService.updateOnlineRolesWithNewValues(newRoles) + await featureService.updateOnlineRolesWithNewValues(newRoles) expect(spy.mock.calls[1][0]).toEqual(FeaturesEvent.DidPurchaseSubscription) }) it('should not notify of subscription purchase on initial roles load after sign in', async () => { storageService.getValue = jest.fn().mockReturnValue(roles) - const featuresService = createService() - featuresService.initializeFromDisk() - featuresService['onlineRoles'] = [] - const spy = jest.spyOn(featuresService, 'notifyEvent' as never) + featureService.initializeFromDisk() + featureService['onlineRoles'] = [] + + const spy = jest.spyOn(featureService, 'notifyEvent' as never) const newRoles = [...roles, RoleName.NAMES.ProUser] - await featuresService.updateOnlineRolesWithNewValues(newRoles) + await featureService.updateOnlineRolesWithNewValues(newRoles) const triggeredEvents = spy.mock.calls.map((call) => call[0]) expect(triggeredEvents).not.toContain(FeaturesEvent.DidPurchaseSubscription) @@ -189,11 +248,11 @@ describe('FeaturesService', () => { it('saves new roles to storage if a role has been added', async () => { storageService.getValue = jest.fn().mockReturnValue(roles) - const featuresService = createService() - featuresService.initializeFromDisk() + + featureService.initializeFromDisk() const newRoles = [...roles, RoleName.NAMES.ProUser] - await featuresService.updateOnlineRolesWithNewValues(newRoles) + await featureService.updateOnlineRolesWithNewValues(newRoles) expect(storageService.setValue).toHaveBeenCalledWith(StorageKey.UserRoles, newRoles) }) @@ -201,134 +260,162 @@ describe('FeaturesService', () => { const newRoles = [RoleName.NAMES.CoreUser] storageService.getValue = jest.fn().mockReturnValue(roles) - const featuresService = createService() - featuresService.initializeFromDisk() - await featuresService.updateOnlineRolesWithNewValues(newRoles) + + featureService.initializeFromDisk() + await featureService.updateOnlineRolesWithNewValues(newRoles) expect(storageService.setValue).toHaveBeenCalledWith(StorageKey.UserRoles, newRoles) }) it('role-based feature status', async () => { - const featuresService = createService() - sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true) - await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser]) + await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser]) subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true) - expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled) - expect(featuresService.getFeatureStatus(FeatureIdentifier.SuperEditor)).toBe(FeatureStatus.Entitled) + expect( + featureService.getFeatureStatus( + NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.MidnightTheme).getValue(), + ), + ).toBe(FeatureStatus.Entitled) + expect( + featureService.getFeatureStatus( + NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SuperEditor).getValue(), + ), + ).toBe(FeatureStatus.Entitled) }) it('feature status with no paid role', async () => { - const featuresService = createService() - sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true) - await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser]) + await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser]) subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(false) - expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.NoUserSubscription) - expect(featuresService.getFeatureStatus(FeatureIdentifier.PlusEditor)).toBe(FeatureStatus.NoUserSubscription) - expect(featuresService.getFeatureStatus(FeatureIdentifier.SheetsEditor)).toBe(FeatureStatus.NoUserSubscription) + expect( + featureService.getFeatureStatus( + NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.MidnightTheme).getValue(), + ), + ).toBe(FeatureStatus.NoUserSubscription) + expect( + featureService.getFeatureStatus( + NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.PlusEditor).getValue(), + ), + ).toBe(FeatureStatus.NoUserSubscription) + expect( + featureService.getFeatureStatus( + NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SheetsEditor).getValue(), + ), + ).toBe(FeatureStatus.NoUserSubscription) }) it('role-based features while not signed into first party server', async () => { - const featuresService = createService() - sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false) - await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.ProUser]) + await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.ProUser]) - expect(featuresService.getFeatureStatus(FeatureIdentifier.SuperEditor)).toBe(FeatureStatus.NoUserSubscription) + expect( + featureService.getFeatureStatus( + NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SuperEditor).getValue(), + ), + ).toBe(FeatureStatus.NoUserSubscription) }) it('third party feature status', async () => { - const featuresService = createService() - itemManager.getDisplayableComponents = jest .fn() - .mockReturnValue([{ identifier: 'third-party-theme' }, { identifier: 'third-party-editor', isExpired: true }]) + .mockReturnValue([ + { uuid: '00000000-0000-0000-0000-000000000001' }, + { uuid: '00000000-0000-0000-0000-000000000002', isExpired: true }, + ]) - await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser]) + await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser]) - expect(featuresService.getFeatureStatus('third-party-theme' as FeatureIdentifier)).toBe(FeatureStatus.Entitled) - expect(featuresService.getFeatureStatus('third-party-editor' as FeatureIdentifier)).toBe( + expect(featureService.getFeatureStatus(Uuid.create('00000000-0000-0000-0000-000000000001').getValue())).toBe( + FeatureStatus.Entitled, + ) + expect(featureService.getFeatureStatus(Uuid.create('00000000-0000-0000-0000-000000000002').getValue())).toBe( FeatureStatus.InCurrentPlanButExpired, ) - expect(featuresService.getFeatureStatus('missing-feature-identifier' as FeatureIdentifier)).toBe( + expect(featureService.getFeatureStatus(Uuid.create('00000000-0000-0000-0000-000000000003').getValue())).toBe( FeatureStatus.NoUserSubscription, ) }) it('feature status should be not entitled if no account or offline repo', async () => { - const featuresService = createService() - - await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser]) + await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser]) sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false) - expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.NoUserSubscription) - expect(featuresService.getFeatureStatus(FeatureIdentifier.TokenVaultEditor)).toBe( - FeatureStatus.NoUserSubscription, - ) + expect( + featureService.getFeatureStatus( + NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.MidnightTheme).getValue(), + ), + ).toBe(FeatureStatus.NoUserSubscription) + expect( + featureService.getFeatureStatus( + NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.TokenVaultEditor).getValue(), + ), + ).toBe(FeatureStatus.NoUserSubscription) }) it('feature status for offline subscription', async () => { - const featuresService = createService() + featureService.hasFirstPartyOfflineSubscription = jest.fn().mockReturnValue(true) + featureService.setOfflineRoles([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser]) - featuresService.hasFirstPartyOfflineSubscription = jest.fn().mockReturnValue(true) - featuresService.setOfflineRoles([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser]) - - expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled) - expect(featuresService.getFeatureStatus(FeatureIdentifier.TokenVaultEditor)).toBe(FeatureStatus.Entitled) + expect( + featureService.getFeatureStatus( + NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.MidnightTheme).getValue(), + ), + ).toBe(FeatureStatus.Entitled) + expect( + featureService.getFeatureStatus( + NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.TokenVaultEditor).getValue(), + ), + ).toBe(FeatureStatus.Entitled) }) it('feature status for deprecated feature and no subscription', async () => { - const featuresService = createService() - subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(false) sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true) - expect(featuresService.getFeatureStatus(FeatureIdentifier.DeprecatedFileSafe as FeatureIdentifier)).toBe( - FeatureStatus.NoUserSubscription, - ) + expect( + featureService.getFeatureStatus( + NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.DeprecatedFileSafe).getValue(), + ), + ).toBe(FeatureStatus.NoUserSubscription) }) it('feature status for deprecated feature with subscription', async () => { - const featuresService = createService() - subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true) - await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser]) + await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser]) - expect(featuresService.getFeatureStatus(FeatureIdentifier.DeprecatedFileSafe as FeatureIdentifier)).toBe( - FeatureStatus.Entitled, - ) + expect( + featureService.getFeatureStatus( + NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.DeprecatedFileSafe).getValue(), + ), + ).toBe(FeatureStatus.Entitled) }) it('has paid subscription', async () => { - const featuresService = createService() - - await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser]) + await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser]) sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true) + subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true) - expect(featuresService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toBeFalsy + expect(featureService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toBeFalsy - await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser]) + await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser]) - expect(featuresService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true) + expect(featureService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true) }) it('has paid subscription should be true if offline repo and signed into third party server', async () => { - const featuresService = createService() + await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser]) - await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser]) - - featuresService.hasOfflineRepo = jest.fn().mockReturnValue(true) + featureService.hasOfflineRepo = jest.fn().mockReturnValue(true) sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false) - expect(featuresService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true) + expect(featureService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true) }) }) @@ -343,8 +430,7 @@ describe('FeaturesService', () => { }, } as never) - const featuresService = createService() - await featuresService.migrateFeatureRepoToUserSetting([extensionRepoItem]) + await featureService.migrateFeatureRepoToUserSetting([extensionRepoItem]) expect(settingsService.updateSetting).toHaveBeenCalledWith( SettingName.create(SettingName.NAMES.ExtensionKey).getValue(), extensionKey, @@ -369,8 +455,7 @@ describe('FeaturesService', () => { const installUrl = 'http://example.com' crypto.base64Decode = jest.fn().mockReturnValue(installUrl) - const featuresService = createService() - const result = await featuresService.downloadRemoteThirdPartyFeature(installUrl) + const result = await featureService.downloadRemoteThirdPartyFeature(installUrl) expect(result).toBeUndefined() }) @@ -389,17 +474,14 @@ describe('FeaturesService', () => { const installUrl = 'http://example.com' crypto.base64Decode = jest.fn().mockReturnValue(installUrl) - const featuresService = createService() - const result = await featuresService.downloadRemoteThirdPartyFeature(installUrl) + const result = await featureService.downloadRemoteThirdPartyFeature(installUrl) expect(result).toBeUndefined() }) }) describe('sortRolesByHierarchy', () => { it('should sort given roles according to role hierarchy', () => { - const featuresService = createService() - - const sortedRoles = featuresService.rolesBySorting([ + const sortedRoles = featureService.rolesBySorting([ RoleName.NAMES.ProUser, RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser, @@ -411,50 +493,42 @@ describe('FeaturesService', () => { describe('hasMinimumRole', () => { it('should be false if core user checks for plus role', async () => { - const featuresService = createService() + await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser]) - await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser]) - - const hasPlusUserRole = featuresService.hasMinimumRole(RoleName.NAMES.PlusUser) + const hasPlusUserRole = featureService.hasMinimumRole(RoleName.NAMES.PlusUser) expect(hasPlusUserRole).toBe(false) }) it('should be false if plus user checks for pro role', async () => { - const featuresService = createService() - sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true) subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true) - await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.PlusUser, RoleName.NAMES.CoreUser]) + await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.PlusUser, RoleName.NAMES.CoreUser]) - const hasProUserRole = featuresService.hasMinimumRole(RoleName.NAMES.ProUser) + const hasProUserRole = featureService.hasMinimumRole(RoleName.NAMES.ProUser) expect(hasProUserRole).toBe(false) }) it('should be true if pro user checks for core user', async () => { - const featuresService = createService() - sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true) subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true) - await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser]) + await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser]) - const hasCoreUserRole = featuresService.hasMinimumRole(RoleName.NAMES.CoreUser) + const hasCoreUserRole = featureService.hasMinimumRole(RoleName.NAMES.CoreUser) expect(hasCoreUserRole).toBe(true) }) it('should be true if pro user checks for pro user', async () => { - const featuresService = createService() - sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true) subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true) - await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser]) + await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser]) - const hasProUserRole = featuresService.hasMinimumRole(RoleName.NAMES.ProUser) + const hasProUserRole = featureService.hasMinimumRole(RoleName.NAMES.ProUser) expect(hasProUserRole).toBe(true) }) diff --git a/packages/snjs/lib/Services/Features/FeaturesService.ts b/packages/snjs/lib/Services/Features/FeaturesService.ts index 1261357eb..3218617d5 100644 --- a/packages/snjs/lib/Services/Features/FeaturesService.ts +++ b/packages/snjs/lib/Services/Features/FeaturesService.ts @@ -1,14 +1,14 @@ import { MigrateFeatureRepoToUserSettingUseCase } from './UseCase/MigrateFeatureRepoToUserSetting' import { arraysEqual, removeFromArray, lastElement } from '@standardnotes/utils' import { ClientDisplayableError } from '@standardnotes/responses' -import { RoleName, ContentType } from '@standardnotes/domain-core' +import { RoleName, ContentType, Uuid } from '@standardnotes/domain-core' import { PROD_OFFLINE_FEATURES_URL } from '../../Hosts' import { PureCryptoInterface } from '@standardnotes/sncrypto-common' import { WebSocketsService } from '../Api/WebsocketsService' import { WebSocketsServiceEvent } from '../Api/WebSocketsServiceEvent' import { TRUSTED_CUSTOM_EXTENSIONS_HOSTS, TRUSTED_FEATURE_HOSTS } from '@Lib/Hosts' import { UserRolesChangedEvent } from '@standardnotes/domain-events' -import { ExperimentalFeatures, FindNativeFeature, FeatureIdentifier } from '@standardnotes/features' +import { ExperimentalFeatures, FindNativeFeature, NativeFeatureIdentifier } from '@standardnotes/features' import { SNFeatureRepo, FeatureRepoContent, @@ -64,7 +64,7 @@ export class FeaturesService { private onlineRoles: string[] = [] private offlineRoles: string[] = [] - private enabledExperimentalFeatures: FeatureIdentifier[] = [] + private enabledExperimentalFeatures: string[] = [] private getFeatureStatusUseCase = new GetFeatureStatusUseCase(this.items) @@ -136,40 +136,47 @@ export class FeaturesService ) } - public initializeFromDisk(): void { + initializeFromDisk(): void { this.onlineRoles = this.storage.getValue(StorageKey.UserRoles, undefined, []) - this.offlineRoles = this.storage.getValue(StorageKey.OfflineUserRoles, undefined, []) - this.enabledExperimentalFeatures = this.storage.getValue(StorageKey.ExperimentalFeatures, undefined, []) } async handleEvent(event: InternalEventInterface): Promise { - if (event.type === ApiServiceEvent.MetaReceived) { - if (!this.sync) { - this.log('Handling events interrupted. Sync service is not yet initialized.', event) - return + switch (event.type) { + case ApiServiceEvent.MetaReceived: { + if (!this.sync) { + this.log('Handling events interrupted. Sync service is not yet initialized.', event) + return + } + + const { userRoles } = event.payload as MetaReceivedData + void this.updateOnlineRolesWithNewValues(userRoles.map((role) => role.name)) + break } - const { userRoles } = event.payload as MetaReceivedData - void this.updateOnlineRolesWithNewValues(userRoles.map((role) => role.name)) - } - - if (event.type === ApplicationEvent.ApplicationStageChanged) { - const stage = (event.payload as ApplicationStageChangedEventPayload).stage - if (stage === ApplicationStage.FullSyncCompleted_13) { - if (!this.hasFirstPartyOnlineSubscription()) { - const offlineRepo = this.getOfflineRepo() - - if (offlineRepo) { - void this.downloadOfflineRoles(offlineRepo) + case ApplicationEvent.ApplicationStageChanged: { + const stage = (event.payload as ApplicationStageChangedEventPayload).stage + switch (stage) { + case ApplicationStage.StorageDecrypted_09: { + this.initializeFromDisk() + break + } + case ApplicationStage.FullSyncCompleted_13: { + if (!this.hasFirstPartyOnlineSubscription()) { + const offlineRepo = this.getOfflineRepo() + if (offlineRepo) { + void this.downloadOfflineRoles(offlineRepo) + } + } + break } } } } } - public enableExperimentalFeature(identifier: FeatureIdentifier): void { + public enableExperimentalFeature(identifier: string): void { this.enabledExperimentalFeatures.push(identifier) void this.storage.setValue(StorageKey.ExperimentalFeatures, this.enabledExperimentalFeatures) @@ -177,7 +184,7 @@ export class FeaturesService void this.notifyEvent(FeaturesEvent.FeaturesAvailabilityChanged) } - public disableExperimentalFeature(identifier: FeatureIdentifier): void { + public disableExperimentalFeature(identifier: string): void { removeFromArray(this.enabledExperimentalFeatures, identifier) void this.storage.setValue(StorageKey.ExperimentalFeatures, this.enabledExperimentalFeatures) @@ -195,7 +202,7 @@ export class FeaturesService void this.notifyEvent(FeaturesEvent.FeaturesAvailabilityChanged) } - public toggleExperimentalFeature(identifier: FeatureIdentifier): void { + public toggleExperimentalFeature(identifier: string): void { if (this.isExperimentalFeatureEnabled(identifier)) { this.disableExperimentalFeature(identifier) } else { @@ -203,19 +210,19 @@ export class FeaturesService } } - public getExperimentalFeatures(): FeatureIdentifier[] { + public getExperimentalFeatures(): string[] { return ExperimentalFeatures } - public isExperimentalFeature(featureId: FeatureIdentifier): boolean { + public isExperimentalFeature(featureId: string): boolean { return this.getExperimentalFeatures().includes(featureId) } - public getEnabledExperimentalFeatures(): FeatureIdentifier[] { + public getEnabledExperimentalFeatures(): string[] { return this.enabledExperimentalFeatures } - public isExperimentalFeatureEnabled(featureId: FeatureIdentifier): boolean { + public isExperimentalFeatureEnabled(featureId: string): boolean { return this.enabledExperimentalFeatures.includes(featureId) } @@ -302,10 +309,10 @@ export class FeaturesService } hasPaidAnyPartyOnlineOrOfflineSubscription(): boolean { - return this.onlineRolesIncludePaidSubscription() || this.hasOfflineRepo() + return this.onlineRolesIncludePaidSubscription() || this.hasOfflineRepo() || this.hasFirstPartyOnlineSubscription() } - private hasFirstPartyOnlineSubscription(): boolean { + hasFirstPartyOnlineSubscription(): boolean { return this.sessions.isSignedIntoFirstPartyServer() && this.subscriptions.hasOnlineSubscription() } @@ -364,12 +371,13 @@ export class FeaturesService } public isThirdPartyFeature(identifier: string): boolean { - const isNativeFeature = !!FindNativeFeature(identifier as FeatureIdentifier) + const isNativeFeature = !!FindNativeFeature(identifier) return !isNativeFeature } onlineRolesIncludePaidSubscription(): boolean { const unpaidRoles = [RoleName.NAMES.CoreUser] + return this.onlineRoles.some((role) => !unpaidRoles.includes(role)) } @@ -392,7 +400,7 @@ export class FeaturesService } public getFeatureStatus( - featureId: FeatureIdentifier, + featureId: NativeFeatureIdentifier | Uuid, options: { inContextOfItem?: DecryptedItemInterface } = {}, ): FeatureStatus { return this.getFeatureStatusUseCase.execute({ diff --git a/packages/snjs/lib/Services/Features/UseCase/GetFeatureStatus.spec.ts b/packages/snjs/lib/Services/Features/UseCase/GetFeatureStatus.spec.ts index 37c9eb753..c799fdd5a 100644 --- a/packages/snjs/lib/Services/Features/UseCase/GetFeatureStatus.spec.ts +++ b/packages/snjs/lib/Services/Features/UseCase/GetFeatureStatus.spec.ts @@ -1,28 +1,23 @@ -import { FeatureIdentifier } from '@standardnotes/features' +import { NativeFeatureIdentifier } from '@standardnotes/features' import { FeatureStatus, ItemManagerInterface } from '@standardnotes/services' import { GetFeatureStatusUseCase } from './GetFeatureStatus' import { ComponentInterface, DecryptedItemInterface } from '@standardnotes/models' - -jest.mock('@standardnotes/features', () => ({ - FeatureIdentifier: { - DarkTheme: 'darkTheme', - }, - FindNativeFeature: jest.fn(), -})) - -import { FindNativeFeature } from '@standardnotes/features' import { Subscription } from '@standardnotes/responses' +import { Uuid } from '@standardnotes/domain-core' describe('GetFeatureStatusUseCase', () => { let items: jest.Mocked let usecase: GetFeatureStatusUseCase + let findNativeFeature: jest.Mock beforeEach(() => { items = { getDisplayableComponents: jest.fn(), } as unknown as jest.Mocked usecase = new GetFeatureStatusUseCase(items) - ;(FindNativeFeature as jest.Mock).mockReturnValue(undefined) + findNativeFeature = jest.fn() + usecase.findNativeFeature = findNativeFeature + findNativeFeature.mockReturnValue(undefined) }) afterEach(() => { @@ -33,7 +28,7 @@ describe('GetFeatureStatusUseCase', () => { it('should return entitled for free features', () => { expect( usecase.execute({ - featureId: FeatureIdentifier.DarkTheme, + featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.DarkTheme).getValue(), hasPaidAnyPartyOnlineOrOfflineSubscription: false, firstPartyOnlineSubscription: undefined, firstPartyRoles: undefined, @@ -44,11 +39,11 @@ describe('GetFeatureStatusUseCase', () => { describe('deprecated features', () => { it('should return entitled for deprecated paid features if any subscription is active', () => { - ;(FindNativeFeature as jest.Mock).mockReturnValue({ deprecated: true }) + findNativeFeature.mockReturnValue({ deprecated: true }) expect( usecase.execute({ - featureId: 'deprecatedFeature', + featureId: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), hasPaidAnyPartyOnlineOrOfflineSubscription: true, firstPartyOnlineSubscription: undefined, firstPartyRoles: undefined, @@ -57,11 +52,11 @@ describe('GetFeatureStatusUseCase', () => { }) it('should return NoUserSubscription for deprecated paid features if no subscription is active', () => { - ;(FindNativeFeature as jest.Mock).mockReturnValue({ deprecated: true }) + findNativeFeature.mockReturnValue({ deprecated: true }) expect( usecase.execute({ - featureId: 'deprecatedFeature', + featureId: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), hasPaidAnyPartyOnlineOrOfflineSubscription: false, firstPartyOnlineSubscription: undefined, firstPartyRoles: undefined, @@ -72,11 +67,11 @@ describe('GetFeatureStatusUseCase', () => { describe('native features', () => { it('should return Entitled if the context item belongs to a shared vault and user does not have subscription', () => { - ;(FindNativeFeature as jest.Mock).mockReturnValue({ deprecated: false }) + findNativeFeature.mockReturnValue({ deprecated: false }) expect( usecase.execute({ - featureId: 'nativeFeature', + featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.AutobiographyTheme).getValue(), firstPartyOnlineSubscription: undefined, firstPartyRoles: undefined, hasPaidAnyPartyOnlineOrOfflineSubscription: false, @@ -86,11 +81,11 @@ describe('GetFeatureStatusUseCase', () => { }) it('should return NoUserSubscription if the context item does not belong to a shared vault and user does not have subscription', () => { - ;(FindNativeFeature as jest.Mock).mockReturnValue({ deprecated: false }) + findNativeFeature.mockReturnValue({ deprecated: false }) expect( usecase.execute({ - featureId: 'nativeFeature', + featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.AutobiographyTheme).getValue(), firstPartyOnlineSubscription: undefined, firstPartyRoles: undefined, hasPaidAnyPartyOnlineOrOfflineSubscription: false, @@ -100,11 +95,11 @@ describe('GetFeatureStatusUseCase', () => { }) it('should return NoUserSubscription for native features without subscription and roles', () => { - ;(FindNativeFeature as jest.Mock).mockReturnValue({ deprecated: false }) + findNativeFeature.mockReturnValue({ deprecated: false }) expect( usecase.execute({ - featureId: 'nativeFeature', + featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.AutobiographyTheme).getValue(), firstPartyOnlineSubscription: undefined, firstPartyRoles: undefined, hasPaidAnyPartyOnlineOrOfflineSubscription: false, @@ -113,14 +108,14 @@ describe('GetFeatureStatusUseCase', () => { }) it('should return NotInCurrentPlan for native features with roles not in available roles', () => { - ;(FindNativeFeature as jest.Mock).mockReturnValue({ + findNativeFeature.mockReturnValue({ deprecated: false, availableInRoles: ['notInRole'], }) expect( usecase.execute({ - featureId: 'nativeFeature', + featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.AutobiographyTheme).getValue(), firstPartyOnlineSubscription: undefined, firstPartyRoles: { online: ['inRole'] }, hasPaidAnyPartyOnlineOrOfflineSubscription: false, @@ -129,14 +124,14 @@ describe('GetFeatureStatusUseCase', () => { }) it('should return Entitled for native features with roles in available roles and active subscription', () => { - ;(FindNativeFeature as jest.Mock).mockReturnValue({ + findNativeFeature.mockReturnValue({ deprecated: false, availableInRoles: ['inRole'], }) expect( usecase.execute({ - featureId: 'nativeFeature', + featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.AutobiographyTheme).getValue(), firstPartyOnlineSubscription: { endsAt: new Date(Date.now() + 10000).getTime(), } as jest.Mocked, @@ -147,14 +142,14 @@ describe('GetFeatureStatusUseCase', () => { }) it('should return InCurrentPlanButExpired for native features with roles in available roles and expired subscription', () => { - ;(FindNativeFeature as jest.Mock).mockReturnValue({ + findNativeFeature.mockReturnValue({ deprecated: false, availableInRoles: ['inRole'], }) expect( usecase.execute({ - featureId: 'nativeFeature', + featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.AutobiographyTheme).getValue(), firstPartyOnlineSubscription: { endsAt: new Date(Date.now() - 10000).getTime(), } as jest.Mocked, @@ -168,7 +163,7 @@ describe('GetFeatureStatusUseCase', () => { describe('third party features', () => { it('should return Entitled for third-party features', () => { const mockComponent = { - identifier: 'thirdPartyFeature', + uuid: '00000000-0000-0000-0000-000000000000', isExpired: false, } as unknown as jest.Mocked @@ -176,7 +171,7 @@ describe('GetFeatureStatusUseCase', () => { expect( usecase.execute({ - featureId: 'thirdPartyFeature', + featureId: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), hasPaidAnyPartyOnlineOrOfflineSubscription: false, firstPartyOnlineSubscription: undefined, firstPartyRoles: undefined, @@ -189,7 +184,7 @@ describe('GetFeatureStatusUseCase', () => { expect( usecase.execute({ - featureId: 'nonExistingThirdPartyFeature', + featureId: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), hasPaidAnyPartyOnlineOrOfflineSubscription: false, firstPartyOnlineSubscription: undefined, firstPartyRoles: undefined, @@ -199,7 +194,7 @@ describe('GetFeatureStatusUseCase', () => { it('should return InCurrentPlanButExpired for expired third-party features', () => { const mockComponent = { - identifier: 'thirdPartyFeature', + uuid: '00000000-0000-0000-0000-000000000000', isExpired: true, } as unknown as jest.Mocked @@ -207,7 +202,7 @@ describe('GetFeatureStatusUseCase', () => { expect( usecase.execute({ - featureId: 'thirdPartyFeature', + featureId: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(), hasPaidAnyPartyOnlineOrOfflineSubscription: false, firstPartyOnlineSubscription: undefined, firstPartyRoles: undefined, diff --git a/packages/snjs/lib/Services/Features/UseCase/GetFeatureStatus.ts b/packages/snjs/lib/Services/Features/UseCase/GetFeatureStatus.ts index 07f276bff..73acb658a 100644 --- a/packages/snjs/lib/Services/Features/UseCase/GetFeatureStatus.ts +++ b/packages/snjs/lib/Services/Features/UseCase/GetFeatureStatus.ts @@ -1,4 +1,5 @@ -import { AnyFeatureDescription, FeatureIdentifier, FindNativeFeature } from '@standardnotes/features' +import { Uuid } from '@standardnotes/domain-core' +import { AnyFeatureDescription, NativeFeatureIdentifier, FindNativeFeature } from '@standardnotes/features' import { DecryptedItemInterface } from '@standardnotes/models' import { Subscription } from '@standardnotes/responses' import { FeatureStatus, ItemManagerInterface } from '@standardnotes/services' @@ -8,20 +9,19 @@ export class GetFeatureStatusUseCase { constructor(private items: ItemManagerInterface) {} execute(dto: { - featureId: FeatureIdentifier | string + featureId: NativeFeatureIdentifier | Uuid firstPartyOnlineSubscription: Subscription | undefined firstPartyRoles: { online: string[] } | { offline: string[] } | undefined hasPaidAnyPartyOnlineOrOfflineSubscription: boolean inContextOfItem?: DecryptedItemInterface }): FeatureStatus { - if (this.isFreeFeature(dto.featureId as FeatureIdentifier)) { + if (this.isFreeFeature(dto.featureId)) { return FeatureStatus.Entitled } - const nativeFeature = FindNativeFeature(dto.featureId as FeatureIdentifier) - + const nativeFeature = this.findNativeFeature(dto.featureId) if (!nativeFeature) { - return this.getThirdPartyFeatureStatus(dto.featureId as string) + return this.getThirdPartyFeatureStatus(dto.featureId) } if (nativeFeature.deprecated) { @@ -39,6 +39,10 @@ export class GetFeatureStatusUseCase { }) } + findNativeFeature(featureId: NativeFeatureIdentifier | Uuid): AnyFeatureDescription | undefined { + return FindNativeFeature(featureId.value) + } + private getDeprecatedNativeFeatureStatus(dto: { hasPaidAnyPartyOnlineOrOfflineSubscription: boolean nativeFeature: AnyFeatureDescription @@ -95,8 +99,8 @@ export class GetFeatureStatusUseCase { return FeatureStatus.Entitled } - private getThirdPartyFeatureStatus(featureId: string): FeatureStatus { - const component = this.items.getDisplayableComponents().find((candidate) => candidate.identifier === featureId) + private getThirdPartyFeatureStatus(uuid: Uuid): FeatureStatus { + const component = this.items.getDisplayableComponents().find((candidate) => candidate.uuid === uuid.value) if (!component) { return FeatureStatus.NoUserSubscription @@ -109,7 +113,9 @@ export class GetFeatureStatusUseCase { return FeatureStatus.Entitled } - private isFreeFeature(featureId: FeatureIdentifier) { - return [FeatureIdentifier.DarkTheme, FeatureIdentifier.PlainEditor].includes(featureId) + private isFreeFeature(featureId: NativeFeatureIdentifier) { + return [NativeFeatureIdentifier.TYPES.DarkTheme, NativeFeatureIdentifier.TYPES.PlainEditor].includes( + featureId.value, + ) } } diff --git a/packages/snjs/lib/Services/Session/SessionManager.ts b/packages/snjs/lib/Services/Session/SessionManager.ts index 795f2d564..ea8da6e92 100644 --- a/packages/snjs/lib/Services/Session/SessionManager.ts +++ b/packages/snjs/lib/Services/Session/SessionManager.ts @@ -29,6 +29,9 @@ import { UserKeyPairChangedEventData, InternalFeatureService, InternalFeature, + ApplicationEvent, + ApplicationStageChangedEventPayload, + ApplicationStage, } from '@standardnotes/services' import { Base64String, PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common' import { @@ -112,8 +115,17 @@ export class SessionManager } async handleEvent(event: InternalEventInterface): Promise { - if (event.type === ApiServiceEvent.SessionRefreshed) { - this.httpService.setSession((event.payload as SessionRefreshedData).session) + switch (event.type) { + case ApiServiceEvent.SessionRefreshed: + this.httpService.setSession((event.payload as SessionRefreshedData).session) + break + + case ApplicationEvent.ApplicationStageChanged: { + const stage = (event.payload as ApplicationStageChangedEventPayload).stage + if (stage === ApplicationStage.StorageDecrypted_09) { + await this.initializeFromDisk() + } + } } } @@ -142,7 +154,7 @@ export class SessionManager this.apiService.setUser(user) } - async initializeFromDisk(): Promise { + private async initializeFromDisk(): Promise { this.memoizeUser(this.storage.getValue(StorageKey.User)) if (!this.user) { diff --git a/packages/snjs/mocha/migrations/migration.test.js b/packages/snjs/mocha/migrations/migration.test.js index 517dc8565..7d4caff8c 100644 --- a/packages/snjs/mocha/migrations/migration.test.js +++ b/packages/snjs/mocha/migrations/migration.test.js @@ -145,7 +145,7 @@ describe('migrations', () => { content_type: ContentType.TYPES.Component, content: FillItemContent({ package_info: { - identifier: FeatureIdentifier.MarkdownProEditor, + identifier: NativeFeatureIdentifier.TYPES.MarkdownProEditor, }, }), }), @@ -170,7 +170,7 @@ describe('migrations', () => { content_type: ContentType.TYPES.Component, content: FillItemContent({ package_info: { - identifier: FeatureIdentifier.DeprecatedBoldEditor, + identifier: NativeFeatureIdentifier.TYPES.DeprecatedBoldEditor, }, }), }), diff --git a/packages/snjs/mocha/model_tests/appmodels.test.js b/packages/snjs/mocha/model_tests/appmodels.test.js index 366eeab97..5e2acd8c5 100644 --- a/packages/snjs/mocha/model_tests/appmodels.test.js +++ b/packages/snjs/mocha/model_tests/appmodels.test.js @@ -369,9 +369,9 @@ describe('app models', () => { editorIdentifier: 'foo-editor', }) - expect(this.application.componentManager.editorForNote(note).uniqueIdentifier).to.equal(component.uuid) + expect(this.application.componentManager.editorForNote(note).uniqueIdentifier.value).to.equal(component.uuid) const duplicate = await this.application.mutator.duplicateItem(note, true) - expect(this.application.componentManager.editorForNote(duplicate).uniqueIdentifier).to.equal(component.uuid) + expect(this.application.componentManager.editorForNote(duplicate).uniqueIdentifier.value).to.equal(component.uuid) }) }) diff --git a/packages/ui-services/package.json b/packages/ui-services/package.json index 8dc7c0114..72798a0dc 100644 --- a/packages/ui-services/package.json +++ b/packages/ui-services/package.json @@ -12,7 +12,7 @@ "scripts": { "tsc": "tsc --project tsconfig.json", "lint": "eslint src --ext .ts", - "test": "jest spec" + "test": "jest" }, "dependencies": { "@standardnotes/common": "^1.50.0", diff --git a/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.spec.ts b/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.spec.ts index 7b6a033b0..71bd24448 100644 --- a/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.spec.ts +++ b/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.spec.ts @@ -1,5 +1,5 @@ import { WebApplicationInterface } from './../../WebApplication/WebApplicationInterface' -import { FeatureIdentifier, NoteType } from '@standardnotes/features' +import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features' import { AegisToAuthenticatorConverter } from './AegisToAuthenticatorConverter' import data from './testData' @@ -57,7 +57,7 @@ describe('AegisConverter', () => { '[{"service":"TestMail","account":"test@test.com","secret":"TESTMAILTESTMAILTESTMAILTESTMAIL","notes":"Some note"},{"service":"Some Service","account":"test@test.com","secret":"SOMESERVICESOMESERVICESOMESERVIC","notes":"Some other service"}]', ) expect(result.content.noteType).toBe(NoteType.Authentication) - expect(result.content.editorIdentifier).toBe(FeatureIdentifier.TokenVaultEditor) + expect(result.content.editorIdentifier).toBe(NativeFeatureIdentifier.TYPES.TokenVaultEditor) }) it('should create note from entries without editor info', () => { diff --git a/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.ts b/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.ts index 0c3058259..009b08000 100644 --- a/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.ts +++ b/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.ts @@ -1,6 +1,6 @@ import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models' import { readFileAsText } from '../Utils' -import { FeatureIdentifier, NoteType } from '@standardnotes/features' +import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features' import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface' import { ContentType } from '@standardnotes/domain-core' @@ -69,7 +69,7 @@ export class AegisToAuthenticatorConverter { references: [], ...(addEditorInfo && { noteType: NoteType.Authentication, - editorIdentifier: FeatureIdentifier.TokenVaultEditor, + editorIdentifier: NativeFeatureIdentifier.TYPES.TokenVaultEditor, }), }, } diff --git a/packages/ui-services/src/Import/Importer.ts b/packages/ui-services/src/Import/Importer.ts index d5ce20d8a..6c959fcb5 100644 --- a/packages/ui-services/src/Import/Importer.ts +++ b/packages/ui-services/src/Import/Importer.ts @@ -1,6 +1,6 @@ import { parseFileName } from '@standardnotes/filepicker' import { FeatureStatus } from '@standardnotes/services' -import { FeatureIdentifier } from '@standardnotes/features' +import { NativeFeatureIdentifier } from '@standardnotes/features' import { AegisToAuthenticatorConverter } from './AegisConverter/AegisToAuthenticatorConverter' import { EvernoteConverter } from './EvernoteConverter/EvernoteConverter' import { GoogleKeepConverter } from './GoogleKeepConverter/GoogleKeepConverter' @@ -64,7 +64,9 @@ export class Importer { async getPayloadsFromFile(file: File, type: NoteImportType): Promise { if (type === 'aegis') { const isEntitledToAuthenticator = - this.application.features.getFeatureStatus(FeatureIdentifier.TokenVaultEditor) === FeatureStatus.Entitled + this.application.features.getFeatureStatus( + NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.TokenVaultEditor).getValue(), + ) === FeatureStatus.Entitled return [await this.aegisConverter.convertAegisBackupFileToNote(file, isEntitledToAuthenticator)] } else if (type === 'google-keep') { return [await this.googleKeepConverter.convertGoogleKeepBackupFileToNote(file, true)] diff --git a/packages/ui-services/src/Theme/ActiveThemeList.spec.ts b/packages/ui-services/src/Theme/ActiveThemeList.spec.ts new file mode 100644 index 000000000..3701cdbc9 --- /dev/null +++ b/packages/ui-services/src/Theme/ActiveThemeList.spec.ts @@ -0,0 +1,58 @@ +import { ActiveThemeList } from './ActiveThemeList' +import { ItemManagerInterface } from '@standardnotes/services' +import { Uuid } from '@standardnotes/domain-core' + +describe('ActiveThemeList', () => { + let itemManager: ItemManagerInterface + let list: ActiveThemeList + + beforeEach(() => { + itemManager = {} as ItemManagerInterface + itemManager.findItem = jest.fn() + + list = new ActiveThemeList(itemManager) + }) + + it('should initialize with an empty list', () => { + expect(list.getList()).toEqual([]) + }) + + it('should be empty initially', () => { + expect(list.isEmpty()).toBe(true) + }) + + it('should not have items that have not been added', () => { + const uuid = Uuid.create('00000000-0000-0000-0000-000000000000').getValue() + expect(list.has(uuid)).toBe(false) + }) + + it('should add an item to the list', () => { + const uuid = Uuid.create('00000000-0000-0000-0000-000000000000').getValue() + list.add(uuid) + expect(list.getList()).toContain(uuid) + expect(list.has(uuid)).toBe(true) + }) + + it('should not add a duplicate item to the list', () => { + const uuid = Uuid.create('00000000-0000-0000-0000-000000000000').getValue() + list.add(uuid) + list.add(uuid) + expect(list.getList()).toEqual([uuid]) + }) + + it('should remove an item from the list', () => { + const uuid = Uuid.create('00000000-0000-0000-0000-000000000000').getValue() + list.add(uuid) + list.remove(uuid) + expect(list.getList()).not.toContain(uuid) + expect(list.has(uuid)).toBe(false) + }) + + it('should clear the list', () => { + const uuid = Uuid.create('00000000-0000-0000-0000-000000000000').getValue() + list.add(uuid) + list.clear() + expect(list.getList()).toEqual([]) + expect(list.has(uuid)).toBe(false) + }) +}) diff --git a/packages/ui-services/src/Theme/ActiveThemeList.ts b/packages/ui-services/src/Theme/ActiveThemeList.ts new file mode 100644 index 000000000..c0521b364 --- /dev/null +++ b/packages/ui-services/src/Theme/ActiveThemeList.ts @@ -0,0 +1,70 @@ +import { UIFeature, ThemeInterface } from '@standardnotes/models' +import { ItemManagerInterface } from '@standardnotes/services' +import { NativeFeatureIdentifier, FindNativeTheme, ThemeFeatureDescription } from '@standardnotes/features' +import { Uuid } from '@standardnotes/domain-core' + +export class ActiveThemeList { + private list: (NativeFeatureIdentifier | Uuid)[] = [] + + constructor(private items: ItemManagerInterface, initialList?: (NativeFeatureIdentifier | Uuid)[]) { + if (initialList) { + this.list = initialList + } + } + + public getList(): (NativeFeatureIdentifier | Uuid)[] { + return this.list.slice() + } + + public isEmpty(): boolean { + return this.list.length === 0 + } + + public clear(): void { + this.list = [] + } + + public has(candidate: NativeFeatureIdentifier | Uuid): boolean { + for (const entry of this.list) { + if (entry.equals(candidate)) { + return true + } + } + + return false + } + + public add(candidate: NativeFeatureIdentifier | Uuid): void { + if (!this.has(candidate)) { + this.list.push(candidate) + } + } + + public remove(candidate: NativeFeatureIdentifier | Uuid): void { + this.list = this.list.filter((entry) => { + return !entry.equals(candidate) + }) + } + + public asThemes(): UIFeature[] { + const results: UIFeature[] = [] + + for (const entry of this.list) { + if (entry instanceof Uuid) { + const theme = this.items.findItem(entry.value) + if (theme) { + const uiFeature = new UIFeature(theme) + results.push(uiFeature) + } + } else { + const theme = FindNativeTheme(entry.value) + if (theme) { + const uiFeature = new UIFeature(theme) + results.push(uiFeature) + } + } + } + + return results + } +} diff --git a/packages/ui-services/src/Theme/ThemeManager.ts b/packages/ui-services/src/Theme/ThemeManager.ts index 25799e405..13a3f2c48 100644 --- a/packages/ui-services/src/Theme/ThemeManager.ts +++ b/packages/ui-services/src/Theme/ThemeManager.ts @@ -6,7 +6,6 @@ import { PrefKey, ThemeInterface, } from '@standardnotes/models' -import { removeFromArray } from '@standardnotes/utils' import { InternalEventBusInterface, ApplicationEvent, @@ -16,17 +15,19 @@ import { PreferencesServiceEvent, ComponentManagerInterface, } from '@standardnotes/services' -import { FeatureIdentifier, FindNativeTheme, ThemeFeatureDescription } from '@standardnotes/features' +import { NativeFeatureIdentifier, FindNativeTheme, ThemeFeatureDescription } from '@standardnotes/features' import { WebApplicationInterface } from '../WebApplication/WebApplicationInterface' import { AbstractUIServicee } from '../Abstract/AbstractUIService' import { GetAllThemesUseCase } from './GetAllThemesUseCase' +import { Uuid } from '@standardnotes/domain-core' +import { ActiveThemeList } from './ActiveThemeList' const CachedThemesKey = 'cachedThemes' const TimeBeforeApplyingColorScheme = 5 const DefaultThemeIdentifier = 'Default' export class ThemeManager extends AbstractUIServicee { - private themesActiveInTheUI: string[] = [] + private themesActiveInTheUI: ActiveThemeList private lastUseDeviceThemeSettings = false constructor( @@ -37,6 +38,23 @@ export class ThemeManager extends AbstractUIServicee { ) { super(application, internalEventBus) this.colorSchemeEventHandler = this.colorSchemeEventHandler.bind(this) + this.themesActiveInTheUI = new ActiveThemeList(application.items) + } + + override deinit() { + this.themesActiveInTheUI.clear() + ;(this.themesActiveInTheUI as unknown) = undefined + ;(this.preferences as unknown) = undefined + ;(this.components as unknown) = undefined + + const mq = window.matchMedia('(prefers-color-scheme: dark)') + if (mq.removeEventListener != undefined) { + mq.removeEventListener('change', this.colorSchemeEventHandler) + } else { + mq.removeListener(this.colorSchemeEventHandler) + } + + super.deinit() } override async onAppStart() { @@ -66,20 +84,32 @@ export class ThemeManager extends AbstractUIServicee { let hasChange = false - const activeThemes = this.components.getActiveThemesIdentifiers() - for (const uiActiveTheme of this.themesActiveInTheUI) { - if (!activeThemes.includes(uiActiveTheme)) { - this.deactivateThemeInTheUI(uiActiveTheme) + const { features, uuids } = this.components.getActiveThemesIdentifiers() + + const featuresList = new ActiveThemeList(this.application.items, features) + const uuidsList = new ActiveThemeList(this.application.items, uuids) + + for (const active of this.themesActiveInTheUI.getList()) { + if (!featuresList.has(active) && !uuidsList.has(active)) { + this.deactivateThemeInTheUI(active) hasChange = true } } - for (const activeTheme of activeThemes) { - if (!this.themesActiveInTheUI.includes(activeTheme)) { - const theme = - FindNativeTheme(activeTheme as FeatureIdentifier) ?? - this.application.items.findItem(activeTheme) + for (const feature of features) { + if (!this.themesActiveInTheUI.has(feature)) { + const theme = FindNativeTheme(feature.value) + if (theme) { + const uiFeature = new UIFeature(theme) + this.activateTheme(uiFeature) + hasChange = true + } + } + } + for (const uuid of uuids) { + if (!this.themesActiveInTheUI.has(uuid)) { + const theme = this.application.items.findItem(uuid.value) if (theme) { const uiFeature = new UIFeature(theme) this.activateTheme(uiFeature) @@ -99,7 +129,7 @@ export class ThemeManager extends AbstractUIServicee { switch (event) { case ApplicationEvent.SignedOut: { this.deactivateAllThemes() - this.themesActiveInTheUI = [] + this.themesActiveInTheUI.clear() this.application?.removeValue(CachedThemesKey, StorageValueModes.Nonwrapped).catch(console.error) break } @@ -158,35 +188,13 @@ export class ThemeManager extends AbstractUIServicee { } } - override deinit() { - this.themesActiveInTheUI = [] - - const mq = window.matchMedia('(prefers-color-scheme: dark)') - if (mq.removeEventListener != undefined) { - mq.removeEventListener('change', this.colorSchemeEventHandler) - } else { - mq.removeListener(this.colorSchemeEventHandler) - } - - super.deinit() - } - private handleFeaturesAvailabilityChanged(): void { let hasChange = false - for (const themeUuid of this.themesActiveInTheUI) { - const theme = this.application.items.findItem(themeUuid) - - if (!theme) { - this.deactivateThemeInTheUI(themeUuid) - hasChange = true - - continue - } - - const status = this.application.features.getFeatureStatus(theme.identifier) + for (const theme of this.themesActiveInTheUI.asThemes()) { + const status = this.application.features.getFeatureStatus(theme.uniqueIdentifier) if (status !== FeatureStatus.Entitled) { - this.deactivateThemeInTheUI(theme.uuid) + this.deactivateThemeInTheUI(theme.uniqueIdentifier) hasChange = true } } @@ -194,7 +202,7 @@ export class ThemeManager extends AbstractUIServicee { const activeThemes = this.components.getActiveThemes() for (const theme of activeThemes) { - if (!this.themesActiveInTheUI.includes(theme.uniqueIdentifier)) { + if (!this.themesActiveInTheUI.has(theme.uniqueIdentifier)) { this.activateTheme(theme) hasChange = true } @@ -245,7 +253,7 @@ export class ThemeManager extends AbstractUIServicee { const preference = prefersDarkColorScheme ? PrefKey.AutoDarkThemeIdentifier : PrefKey.AutoLightThemeIdentifier const preferenceDefault = - preference === PrefKey.AutoDarkThemeIdentifier ? FeatureIdentifier.DarkTheme : DefaultThemeIdentifier + preference === PrefKey.AutoDarkThemeIdentifier ? NativeFeatureIdentifier.TYPES.DarkTheme : DefaultThemeIdentifier const usecase = new GetAllThemesUseCase(this.application.items) const { thirdParty, native } = usecase.execute({ excludeLayerable: false }) @@ -289,21 +297,20 @@ export class ThemeManager extends AbstractUIServicee { } private deactivateAllThemes() { - const activeThemes = this.themesActiveInTheUI.slice() - + const activeThemes = this.themesActiveInTheUI.getList() for (const uuid of activeThemes) { this.deactivateThemeInTheUI(uuid) } } private activateTheme(theme: UIFeature, skipEntitlementCheck = false) { - if (this.themesActiveInTheUI.find((uuid) => uuid === theme.uniqueIdentifier)) { + if (this.themesActiveInTheUI.has(theme.uniqueIdentifier)) { return } if ( !skipEntitlementCheck && - this.application.features.getFeatureStatus(theme.featureIdentifier) !== FeatureStatus.Entitled + this.application.features.getFeatureStatus(theme.uniqueIdentifier) !== FeatureStatus.Entitled ) { return } @@ -313,14 +320,14 @@ export class ThemeManager extends AbstractUIServicee { return } - this.themesActiveInTheUI.push(theme.uniqueIdentifier) + this.themesActiveInTheUI.add(theme.uniqueIdentifier) const link = document.createElement('link') link.href = url link.type = 'text/css' link.rel = 'stylesheet' link.media = 'screen,print' - link.id = theme.uniqueIdentifier + link.id = theme.uniqueIdentifier.value link.onload = () => { this.syncThemeColorMetadata() @@ -336,20 +343,20 @@ export class ThemeManager extends AbstractUIServicee { document.getElementsByTagName('head')[0].appendChild(link) } - private deactivateThemeInTheUI(uuid: string) { - if (!this.themesActiveInTheUI.includes(uuid)) { + private deactivateThemeInTheUI(id: NativeFeatureIdentifier | Uuid) { + if (!this.themesActiveInTheUI.has(id)) { return } - const element = document.getElementById(uuid) as HTMLLinkElement + const element = document.getElementById(id.value) as HTMLLinkElement if (element) { element.disabled = true element.parentNode?.removeChild(element) } - removeFromArray(this.themesActiveInTheUI, uuid) + this.themesActiveInTheUI.remove(id) - if (this.themesActiveInTheUI.length === 0 && this.application.isNativeMobileWeb()) { + if (this.themesActiveInTheUI.isEmpty() && this.application.isNativeMobileWeb()) { this.application.mobileDevice().handleThemeSchemeChange(false, '#ffffff') } } @@ -373,11 +380,16 @@ export class ThemeManager extends AbstractUIServicee { } private async cacheThemeState() { - const themes = this.application.items.findItems(this.themesActiveInTheUI) + const themes = this.themesActiveInTheUI.asThemes() const mapped = themes.map((theme) => { - const payload = theme.payloadRepresentation() - return CreateDecryptedLocalStorageContextPayload(payload) + if (theme.isComponent) { + const payload = theme.asComponent.payloadRepresentation() + return CreateDecryptedLocalStorageContextPayload(payload) + } else { + const payload = theme.asFeatureDescription + return payload + } }) return this.application.setValue(CachedThemesKey, mapped, StorageValueModes.Nonwrapped) @@ -389,16 +401,25 @@ export class ThemeManager extends AbstractUIServicee { StorageValueModes.Nonwrapped, ) - if (cachedThemes) { - const themes: ThemeInterface[] = [] - for (const cachedTheme of cachedThemes) { - const payload = this.application.items.createPayloadFromObject(cachedTheme) - const theme = this.application.items.createItemFromPayload(payload) - themes.push(theme) - } - return themes.map((theme) => new UIFeature(theme)) - } else { + if (!cachedThemes) { return [] } + + const features: UIFeature[] = [] + + for (const cachedTheme of cachedThemes) { + if ('uuid' in cachedTheme) { + const payload = this.application.items.createPayloadFromObject(cachedTheme) + const theme = this.application.items.createItemFromPayload(payload) + features.push(new UIFeature(theme)) + } else if ('identifier' in cachedTheme) { + const feature = FindNativeTheme((cachedTheme as ThemeFeatureDescription).identifier) + if (feature) { + features.push(new UIFeature(feature)) + } + } + } + + return features } } diff --git a/packages/utils/package.json b/packages/utils/package.json index d2b960695..8401a92c5 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -22,7 +22,7 @@ "prebuild": "yarn clean", "build": "tsc -p tsconfig.json", "lint": "eslint src --ext .ts", - "test": "jest spec" + "test": "jest" }, "dependencies": { "@standardnotes/common": "^1.50.0", diff --git a/packages/utils/src/Domain/Utils/Utils.spec.ts b/packages/utils/src/Domain/Utils/Utils.spec.ts index 306ac3476..b867dce91 100644 --- a/packages/utils/src/Domain/Utils/Utils.spec.ts +++ b/packages/utils/src/Domain/Utils/Utils.spec.ts @@ -1,11 +1,46 @@ import * as DOMPurifyLib from 'dompurify' import { JSDOM } from 'jsdom' -import { sortByKey, withoutLastElement } from './Utils' +import { sortByKey, withoutLastElement, compareArrayReferences } from './Utils' const window = new JSDOM('').window const DOMPurify = DOMPurifyLib(window as never) describe('Utils', () => { + describe('compareArrayReferences', () => { + it('should return true when both arrays are empty', () => { + expect(compareArrayReferences([], [])).toBe(true) + }) + + it('should return true when both arrays have the same reference', () => { + const obj = {} + expect(compareArrayReferences([obj], [obj])).toBe(true) + }) + + it('should return false when arrays have different lengths', () => { + const obj1 = {} + const obj2 = {} + expect(compareArrayReferences([obj1], [obj1, obj2])).toBe(false) + }) + + it('should return false when arrays have the same length but different references', () => { + const obj1 = {} + const obj2 = {} + expect(compareArrayReferences([obj1], [obj2])).toBe(false) + }) + + it('should return true when arrays have multiple identical references', () => { + const obj1 = {} + const obj2 = {} + expect(compareArrayReferences([obj1, obj2], [obj1, obj2])).toBe(true) + }) + + it('should return false when arrays have the same references in different order', () => { + const obj1 = {} + const obj2 = {} + expect(compareArrayReferences([obj1, obj2], [obj2, obj1])).toBe(false) + }) + }) + it('sanitizeHtmlString', () => { const dirty = '' const cleaned = DOMPurify.sanitize(dirty) diff --git a/packages/utils/src/Domain/Utils/Utils.ts b/packages/utils/src/Domain/Utils/Utils.ts index b2e9930a0..e5f921dbe 100644 --- a/packages/utils/src/Domain/Utils/Utils.ts +++ b/packages/utils/src/Domain/Utils/Utils.ts @@ -223,6 +223,10 @@ export function arrayByDifference(array: T[], subtract: T[]): T[] { return array.filter((x) => !subtract.includes(x)).concat(subtract.filter((x) => !array.includes(x))) } +export function compareArrayReferences(arr1: T[], arr2: T[]) { + return arr1.length === arr2.length && arr1.every((val, index) => val === arr2[index]) +} + export function compareValues(left: T, right: T) { if ((left && !right) || (!left && right)) { return false diff --git a/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx b/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx index 56e952bc8..7186c525a 100644 --- a/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx +++ b/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx @@ -6,7 +6,7 @@ import { WebApplication } from '@/Application/WebApplication' import { UIFeature, EditorFeatureDescription, - FeatureIdentifier, + NativeFeatureIdentifier, IframeComponentFeatureDescription, NoteMutator, NoteType, @@ -100,7 +100,7 @@ const ChangeEditorMenu: FunctionComponent = ({ setCurrentFeature(application.componentManager.editorForNote(note)) - if (uiFeature.featureIdentifier === FeatureIdentifier.PlainEditor) { + if (uiFeature.featureIdentifier === NativeFeatureIdentifier.TYPES.PlainEditor) { reloadFont(application.getPreference(PrefKey.EditorMonospaceEnabled)) } }, @@ -211,7 +211,7 @@ const ChangeEditorMenu: FunctionComponent = ({ return ( diff --git a/packages/web/src/javascripts/Components/ClipperView/ClipperView.tsx b/packages/web/src/javascripts/Components/ClipperView/ClipperView.tsx index 807124266..46d52849c 100644 --- a/packages/web/src/javascripts/Components/ClipperView/ClipperView.tsx +++ b/packages/web/src/javascripts/Components/ClipperView/ClipperView.tsx @@ -15,7 +15,7 @@ import { ApplicationEvent, ContentType, DecryptedItem, - FeatureIdentifier, + NativeFeatureIdentifier, FeatureStatus, NoteContent, NoteType, @@ -61,7 +61,10 @@ const ClipperView = ({ const [user, setUser] = useState(() => application.getUser()) const [isEntitledToExtension, setIsEntitled] = useState( - () => application.features.getFeatureStatus(FeatureIdentifier.Extension) === FeatureStatus.Entitled, + () => + application.features.getFeatureStatus( + NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.Clipper).getValue(), + ) === FeatureStatus.Entitled, ) const isEntitledRef = useStateRef(isEntitledToExtension) const hasSubscription = application.hasValidFirstPartySubscription() @@ -72,10 +75,18 @@ const ClipperView = ({ case ApplicationEvent.SignedOut: case ApplicationEvent.UserRolesChanged: setUser(application.getUser()) - setIsEntitled(application.features.getFeatureStatus(FeatureIdentifier.Extension) === FeatureStatus.Entitled) + setIsEntitled( + application.features.getFeatureStatus( + NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.Clipper).getValue(), + ) === FeatureStatus.Entitled, + ) break case ApplicationEvent.FeaturesAvailabilityChanged: - setIsEntitled(application.features.getFeatureStatus(FeatureIdentifier.Extension) === FeatureStatus.Entitled) + setIsEntitled( + application.features.getFeatureStatus( + NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.Clipper).getValue(), + ) === FeatureStatus.Entitled, + ) break } }) @@ -212,7 +223,7 @@ const ClipperView = ({ const note = application.items.createTemplateItem(ContentType.TYPES.Note, { title: clipPayload.title, text: editorStateJSON, - editorIdentifier: FeatureIdentifier.SuperEditor, + editorIdentifier: NativeFeatureIdentifier.TYPES.SuperEditor, noteType: NoteType.Super, references: [], }) diff --git a/packages/web/src/javascripts/Components/ComponentView/IframeFeatureView.tsx b/packages/web/src/javascripts/Components/ComponentView/IframeFeatureView.tsx index 5186d5846..1361060e9 100644 --- a/packages/web/src/javascripts/Components/ComponentView/IframeFeatureView.tsx +++ b/packages/web/src/javascripts/Components/ComponentView/IframeFeatureView.tsx @@ -168,7 +168,7 @@ const IframeFeatureView: FunctionComponent = ({ onLoad, componentViewer, const unregisterDesktopObserver = application .getDesktopService() ?.registerUpdateObserver((updatedComponent: ComponentInterface) => { - if (updatedComponent.uuid === uiFeature.uniqueIdentifier) { + if (updatedComponent.uuid === uiFeature.uniqueIdentifier.value) { requestReload?.(componentViewer) } }) diff --git a/packages/web/src/javascripts/Components/ContentListView/Header/NewNotePreferences.tsx b/packages/web/src/javascripts/Components/ContentListView/Header/NewNotePreferences.tsx index d3c46790e..278b3e24b 100644 --- a/packages/web/src/javascripts/Components/ContentListView/Header/NewNotePreferences.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/Header/NewNotePreferences.tsx @@ -1,14 +1,14 @@ import { - FeatureIdentifier, + NativeFeatureIdentifier, NewNoteTitleFormat, PrefKey, - EditorIdentifier, TagPreferences, isSmartView, isSystemView, SystemViewId, PrefDefaults, FeatureStatus, + Uuid, } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' import { ChangeEventHandler, FunctionComponent, useCallback, useEffect, useRef, useState } from 'react' @@ -17,6 +17,11 @@ import { DropdownItem } from '@/Components/Dropdown/DropdownItem' import { WebApplication } from '@/Application/WebApplication' import { AnyTag } from '@/Controllers/Navigation/AnyTagType' import { PreferenceMode } from './PreferenceMode' +import { EditorOption, getDropdownItemsForAllEditors } from '@/Utils/DropdownItemsForEditors' +import { classNames } from '@standardnotes/utils' +import { NoteTitleFormatOptions } from './NoteTitleFormatOptions' +import { usePremiumModal } from '@/Hooks/usePremiumModal' + import dayjs from 'dayjs' import dayjsAdvancedFormat from 'dayjs/plugin/advancedFormat' import dayjsUTC from 'dayjs/plugin/utc' @@ -24,11 +29,6 @@ import dayjsTimezone from 'dayjs/plugin/timezone' dayjs.extend(dayjsAdvancedFormat) dayjs.extend(dayjsUTC) dayjs.extend(dayjsTimezone) -import { EditorOption, getDropdownItemsForAllEditors } from '@/Utils/DropdownItemsForEditors' -import { classNames } from '@standardnotes/utils' -import { NoteTitleFormatOptions } from './NoteTitleFormatOptions' - -import { usePremiumModal } from '@/Hooks/usePremiumModal' const PrefChangeDebounceTimeInMs = 25 @@ -57,8 +57,8 @@ const NewNotePreferences: FunctionComponent = ({ : selectedTag.preferences const [editorItems, setEditorItems] = useState([]) - const [defaultEditorIdentifier, setDefaultEditorIdentifier] = useState( - FeatureIdentifier.PlainEditor, + const [defaultEditorIdentifier, setDefaultEditorIdentifier] = useState( + NativeFeatureIdentifier.TYPES.PlainEditor, ) const [newNoteTitleFormat, setNewNoteTitleFormat] = useState( NewNoteTitleFormat.CurrentDateAndTime, @@ -121,14 +121,19 @@ const NewNotePreferences: FunctionComponent = ({ const selectEditorForNewNoteDefault = useCallback( (value: EditorOption['value']) => { - if (application.features.getFeatureStatus(value) !== FeatureStatus.Entitled) { + const uuid = Uuid.create(value) + const feature = NativeFeatureIdentifier.create(value) + if ( + application.features.getFeatureStatus(!uuid.isFailed() ? uuid.getValue() : feature.getValue()) !== + FeatureStatus.Entitled + ) { const editorItem = editorItems.find((item) => item.value === value) if (editorItem) { premiumModal.activate(editorItem.label) } return } - setDefaultEditorIdentifier(value as FeatureIdentifier) + setDefaultEditorIdentifier(value) if (mode === 'global') { void application.setPreference(PrefKey.DefaultEditorIdentifier, value) diff --git a/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.spec.ts b/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.spec.ts index 50daa491c..b32928d55 100644 --- a/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.spec.ts +++ b/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.spec.ts @@ -1,7 +1,6 @@ import { WebApplication } from '@/Application/WebApplication' import { ContentType } from '@standardnotes/domain-core' import { - MutatorService, SNComponentManager, SNComponent, SNTag, @@ -11,7 +10,7 @@ import { ItemManagerInterface, MutatorClientInterface, } from '@standardnotes/snjs' -import { FeatureIdentifier, NoteType } from '@standardnotes/features' +import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features' import { NoteViewController } from './NoteViewController' describe('note view controller', () => { @@ -40,7 +39,9 @@ describe('note view controller', () => { }) it('should create notes with plaintext note type', async () => { - application.componentManager.getDefaultEditorIdentifier = jest.fn().mockReturnValue(FeatureIdentifier.PlainEditor) + application.componentManager.getDefaultEditorIdentifier = jest + .fn() + .mockReturnValue(NativeFeatureIdentifier.TYPES.PlainEditor) const controller = new NoteViewController(application) await controller.initialize() @@ -55,13 +56,13 @@ describe('note view controller', () => { it('should create notes with markdown note type', async () => { application.items.getDisplayableComponents = jest.fn().mockReturnValue([ { - identifier: FeatureIdentifier.MarkdownProEditor, + identifier: NativeFeatureIdentifier.TYPES.MarkdownProEditor, } as SNComponent, ]) application.componentManager.getDefaultEditorIdentifier = jest .fn() - .mockReturnValue(FeatureIdentifier.MarkdownProEditor) + .mockReturnValue(NativeFeatureIdentifier.TYPES.MarkdownProEditor) const controller = new NoteViewController(application) await controller.initialize() @@ -74,7 +75,9 @@ describe('note view controller', () => { }) it('should add tag to note if default tag is set', async () => { - application.componentManager.getDefaultEditorIdentifier = jest.fn().mockReturnValue(FeatureIdentifier.PlainEditor) + application.componentManager.getDefaultEditorIdentifier = jest + .fn() + .mockReturnValue(NativeFeatureIdentifier.TYPES.PlainEditor) const tag = { uuid: 'tag-uuid', diff --git a/packages/web/src/javascripts/Components/NoteView/NoteView.tsx b/packages/web/src/javascripts/Components/NoteView/NoteView.tsx index acb841eb8..e204d1222 100644 --- a/packages/web/src/javascripts/Components/NoteView/NoteView.tsx +++ b/packages/web/src/javascripts/Components/NoteView/NoteView.tsx @@ -8,7 +8,7 @@ import { ElementIds } from '@/Constants/ElementIDs' import { StringDeleteNote, STRING_DELETE_LOCKED_ATTEMPT, STRING_DELETE_PLACEHOLDER_ATTEMPT } from '@/Constants/Strings' import { log, LoggingDomain } from '@/Logging' import { debounce, isDesktopApplication, isMobileScreen } from '@/Utils' -import { classNames, pluralize } from '@standardnotes/utils' +import { classNames, compareArrayReferences, pluralize } from '@standardnotes/utils' import { ApplicationEvent, ComponentArea, @@ -19,7 +19,6 @@ import { EditorLineWidth, IframeComponentFeatureDescription, isUIFeatureAnIframeFeature, - isPayloadSourceInternalChange, isPayloadSourceRetrieved, NoteType, PayloadEmitSource, @@ -94,7 +93,6 @@ class NoteView extends AbstractComponent { onEditorComponentLoad?: () => void private removeTrashKeyObserver?: () => void - private removeComponentStreamObserver?: () => void private removeNoteStreamObserver?: () => void private removeComponentManagerObserver?: () => void private removeInnerNoteObserver?: () => void @@ -144,9 +142,6 @@ class NoteView extends AbstractComponent { super.deinit() ;(this.controller as unknown) = undefined - this.removeComponentStreamObserver?.() - ;(this.removeComponentStreamObserver as unknown) = undefined - this.removeNoteStreamObserver?.() ;(this.removeNoteStreamObserver as unknown) = undefined @@ -187,14 +182,19 @@ class NoteView extends AbstractComponent { } override shouldComponentUpdate(_nextProps: Readonly, nextState: Readonly): boolean { - const complexObjects: (keyof State)[] = ['availableStackComponents', 'stackComponentViewers'] for (const key of Object.keys(nextState) as (keyof State)[]) { - if (complexObjects.includes(key)) { - continue - } const prevValue = this.state[key] const nextValue = nextState[key] + if (Array.isArray(prevValue) && Array.isArray(nextValue)) { + const areEqual = compareArrayReferences(prevValue, nextValue) + if (!areEqual) { + log(LoggingDomain.NoteView, 'Rendering due to array state change', key, prevValue, nextValue) + return true + } + continue + } + if (prevValue !== nextValue) { log(LoggingDomain.NoteView, 'Rendering due to state change', key, prevValue, nextValue) return true @@ -340,7 +340,8 @@ class NoteView extends AbstractComponent { switch (eventName) { case ApplicationEvent.PreferencesChanged: - this.reloadPreferences().catch(console.error) + void this.reloadPreferences() + void this.reloadStackComponents() break case ApplicationEvent.HighLatencySync: this.setState({ syncTakingTooLong: true }) @@ -428,23 +429,6 @@ class NoteView extends AbstractComponent { } streamItems() { - this.removeComponentStreamObserver = this.application.streamItems( - ContentType.TYPES.Component, - async ({ source }) => { - log(LoggingDomain.NoteView, 'On component stream observer', PayloadEmitSource[source]) - if (isPayloadSourceInternalChange(source) || source === PayloadEmitSource.InitialObserverRegistrationPush) { - return - } - - if (!this.note) { - return - } - - await this.reloadStackComponents() - this.debounceReloadEditorComponent() - }, - ) - this.removeNoteStreamObserver = this.application.streamItems(ContentType.TYPES.Note, async () => { if (!this.note) { return @@ -740,25 +724,22 @@ class NoteView extends AbstractComponent { async reloadStackComponents() { log(LoggingDomain.NoteView, 'Reload stack components') - const stackComponents = sortAlphabetically( + const enabledComponents = sortAlphabetically( this.application.componentManager .thirdPartyComponentsForArea(ComponentArea.EditorStack) .filter((component) => this.application.componentManager.isComponentActive(component)), ) - const enabledComponents = stackComponents.filter((component) => { - return component.isExplicitlyEnabledForItem(this.note.uuid) - }) const needsNewViewer = enabledComponents.filter((component) => { const hasExistingViewer = this.state.stackComponentViewers.find( - (viewer) => viewer.componentUniqueIdentifier === component.uuid, + (viewer) => viewer.componentUniqueIdentifier.value === component.uuid, ) return !hasExistingViewer }) const needsDestroyViewer = this.state.stackComponentViewers.filter((viewer) => { const viewerComponentExistsInEnabledComponents = enabledComponents.find((component) => { - return component.uuid === viewer.componentUniqueIdentifier + return component.uuid === viewer.componentUniqueIdentifier.value }) return !viewerComponentExistsInEnabledComponents }) @@ -779,13 +760,15 @@ class NoteView extends AbstractComponent { this.application.componentManager.destroyComponentViewer(viewer) } this.setState({ - availableStackComponents: stackComponents, + availableStackComponents: enabledComponents, stackComponentViewers: newViewers, }) } stackComponentExpanded = (component: ComponentInterface): boolean => { - return !!this.state.stackComponentViewers.find((viewer) => viewer.componentUniqueIdentifier === component.uuid) + return !!this.state.stackComponentViewers.find( + (viewer) => viewer.componentUniqueIdentifier.value === component.uuid, + ) } toggleStackComponent = async (component: ComponentInterface) => { diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Account/Email/Email.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Account/Email/Email.tsx index 18a46887a..f51d80509 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Account/Email/Email.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Account/Email/Email.tsx @@ -1,5 +1,5 @@ import { - FeatureIdentifier, + NativeFeatureIdentifier, FeatureStatus, MuteMarketingEmailsOption, MuteSignInEmailsOption, @@ -28,7 +28,9 @@ const Email: FunctionComponent = ({ application }: Props) => { const [isLoading, setIsLoading] = useState(true) const isMuteSignInEmailsFeatureAvailable = - application.features.getFeatureStatus(FeatureIdentifier.SignInAlerts) === FeatureStatus.Entitled + application.features.getFeatureStatus( + NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SignInAlerts).getValue(), + ) === FeatureStatus.Entitled const updateSetting = async (settingName: SettingName, payload: string): Promise => { try { diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Account/SubscriptionSharing/SubscriptionSharing.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Account/SubscriptionSharing/SubscriptionSharing.tsx index c5cca1d28..a77fb7efb 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Account/SubscriptionSharing/SubscriptionSharing.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Account/SubscriptionSharing/SubscriptionSharing.tsx @@ -1,4 +1,4 @@ -import { FeatureStatus, FeatureIdentifier } from '@standardnotes/snjs' +import { FeatureStatus, NativeFeatureIdentifier } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' import { FunctionComponent, useState } from 'react' @@ -29,8 +29,9 @@ const SubscriptionSharing: FunctionComponent = ({ application, viewContro const isReadOnlySession = application.sessions.isCurrentSessionReadOnly() const isSubscriptionSharingFeatureAvailable = - application.features.getFeatureStatus(FeatureIdentifier.SubscriptionSharing) === FeatureStatus.Entitled && - !isReadOnlySession + application.features.getFeatureStatus( + NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SubscriptionSharing).getValue(), + ) === FeatureStatus.Entitled && !isReadOnlySession const closeInviteDialog = () => setIsInviteDialogOpen(false) diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Appearance.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Appearance.tsx index d0f95150c..1357f0242 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Appearance.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Appearance.tsx @@ -4,7 +4,7 @@ import { usePremiumModal } from '@/Hooks/usePremiumModal' import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator' import Switch from '@/Components/Switch/Switch' import { WebApplication } from '@/Application/WebApplication' -import { FeatureIdentifier, PrefKey, FeatureStatus, naturalSort, PrefDefaults } from '@standardnotes/snjs' +import { PrefKey, FeatureStatus, naturalSort, PrefDefaults } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' import { FunctionComponent, useEffect, useState } from 'react' import { Subtitle, Title, Text } from '@/Components/Preferences/PreferencesComponents/Content' @@ -50,7 +50,7 @@ const Appearance: FunctionComponent = ({ application }) => { label: theme.displayName as string, value: theme.featureIdentifier, icon: - application.features.getFeatureStatus(theme.featureIdentifier) !== FeatureStatus.Entitled + application.features.getFeatureStatus(theme.uniqueIdentifier) !== FeatureStatus.Entitled ? PremiumFeatureIconName : undefined, } @@ -72,14 +72,10 @@ const Appearance: FunctionComponent = ({ application }) => { const toggleUseDeviceSettings = () => { application.setPreference(PrefKey.UseSystemColorScheme, !useDeviceSettings).catch(console.error) if (!application.getPreference(PrefKey.AutoLightThemeIdentifier)) { - application - .setPreference(PrefKey.AutoLightThemeIdentifier, autoLightTheme as FeatureIdentifier) - .catch(console.error) + application.setPreference(PrefKey.AutoLightThemeIdentifier, autoLightTheme).catch(console.error) } if (!application.getPreference(PrefKey.AutoDarkThemeIdentifier)) { - application - .setPreference(PrefKey.AutoDarkThemeIdentifier, autoDarkTheme as FeatureIdentifier) - .catch(console.error) + application.setPreference(PrefKey.AutoDarkThemeIdentifier, autoDarkTheme).catch(console.error) } setUseDeviceSettings(!useDeviceSettings) } @@ -90,7 +86,7 @@ const Appearance: FunctionComponent = ({ application }) => { premiumModal.activate(`${item.label} theme`) return } - application.setPreference(PrefKey.AutoLightThemeIdentifier, value as FeatureIdentifier).catch(console.error) + application.setPreference(PrefKey.AutoLightThemeIdentifier, value).catch(console.error) setAutoLightTheme(value) } @@ -100,7 +96,7 @@ const Appearance: FunctionComponent = ({ application }) => { premiumModal.activate(`${item.label} theme`) return } - application.setPreference(PrefKey.AutoDarkThemeIdentifier, value as FeatureIdentifier).catch(console.error) + application.setPreference(PrefKey.AutoDarkThemeIdentifier, value).catch(console.error) setAutoDarkTheme(value) } diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/General/Labs/Labs.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/General/Labs/Labs.tsx index 409da2b4b..2292a316c 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/General/Labs/Labs.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/General/Labs/Labs.tsx @@ -2,7 +2,7 @@ import { Text, Title } from '@/Components/Preferences/PreferencesComponents/Cont import { WebApplication } from '@/Application/WebApplication' import { ApplicationEvent, - FeatureIdentifier, + NativeFeatureIdentifier, FeatureStatus, FindNativeFeature, PrefKey, @@ -17,7 +17,7 @@ import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator' import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery' type ExperimentalFeatureItem = { - identifier: FeatureIdentifier + identifier: string name: string description: string isEnabled: boolean @@ -55,7 +55,9 @@ const LabsPane: FunctionComponent = ({ application }) => { name: feature?.name ?? featureIdentifier, description: feature?.description ?? '', isEnabled: application.features.isExperimentalFeatureEnabled(featureIdentifier), - isEntitled: application.features.getFeatureStatus(featureIdentifier) === FeatureStatus.Entitled, + isEntitled: + application.features.getFeatureStatus(NativeFeatureIdentifier.create(featureIdentifier).getValue()) === + FeatureStatus.Entitled, } }) setExperimentalFeatures(experimentalFeatures) diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Security/Security.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Security/Security.tsx index b1e424c7d..ff98ac6aa 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Security/Security.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Security/Security.tsx @@ -1,4 +1,4 @@ -import { FeatureIdentifier, FeatureStatus } from '@standardnotes/snjs' +import { NativeFeatureIdentifier, FeatureStatus } from '@standardnotes/snjs' import { WebApplication } from '@/Application/WebApplication' import { ViewControllerManager } from '@/Controllers/ViewControllerManager' @@ -24,8 +24,9 @@ const Security: FunctionComponent = (props) => { const isNativeMobileWeb = props.application.isNativeMobileWeb() const isU2FFeatureAvailable = - props.application.features.getFeatureStatus(FeatureIdentifier.UniversalSecondFactor) === FeatureStatus.Entitled && - props.userProvider.getUser() !== undefined + props.application.features.getFeatureStatus( + NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.UniversalSecondFactor).getValue(), + ) === FeatureStatus.Entitled && props.userProvider.getUser() !== undefined return ( diff --git a/packages/web/src/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx b/packages/web/src/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx index b6a08bd09..cf8efd859 100644 --- a/packages/web/src/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx +++ b/packages/web/src/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx @@ -3,7 +3,7 @@ import { ComponentInterface, UIFeature, ContentType, - FeatureIdentifier, + NativeFeatureIdentifier, PreferencesServiceEvent, ThemeFeatureDescription, } from '@standardnotes/snjs' @@ -54,7 +54,7 @@ const QuickSettingsMenu: FunctionComponent = ({ quickSettingsMenuCont (component) => !component.isTheme() && [ComponentArea.EditorStack].includes(component.area) && - component.identifier !== FeatureIdentifier.DeprecatedFoldersComponent, + component.identifier !== NativeFeatureIdentifier.TYPES.DeprecatedFoldersComponent, ) setEditorStackComponents(toggleableComponents) @@ -100,7 +100,7 @@ const QuickSettingsMenu: FunctionComponent = ({ quickSettingsMenuCont const toggleEditorStackComponent = useCallback( (component: ComponentInterface) => { - application.componentManager.toggleComponent(component).catch(console.error) + void application.componentManager.toggleComponent(component) }, [application], ) @@ -141,7 +141,7 @@ const QuickSettingsMenu: FunctionComponent = ({ quickSettingsMenuCont Default {themes.map((theme) => ( - + ))} = ({ uiFeature }) => { [application, uiFeature.featureIdentifier], ) const isEntitledToTheme = useMemo( - () => application.features.getFeatureStatus(uiFeature.featureIdentifier) === FeatureStatus.Entitled, - [application, uiFeature.featureIdentifier], + () => application.features.getFeatureStatus(uiFeature.uniqueIdentifier) === FeatureStatus.Entitled, + [application, uiFeature.uniqueIdentifier], ) const canActivateTheme = useMemo(() => isEntitledToTheme || isThirdPartyTheme, [isEntitledToTheme, isThirdPartyTheme]) @@ -55,10 +55,10 @@ const ThemesMenuButton: FunctionComponent = ({ uiFeature }) => { ) const isMobile = application.isNativeMobileWeb() || isMobileScreen() - const shouldHideButton = uiFeature.featureIdentifier === FeatureIdentifier.DynamicTheme && isMobile + const shouldHideButton = uiFeature.featureIdentifier === NativeFeatureIdentifier.TYPES.DynamicTheme && isMobile const darkThemeShortcut = useMemo(() => { - if (uiFeature.featureIdentifier === FeatureIdentifier.DarkTheme) { + if (uiFeature.featureIdentifier === NativeFeatureIdentifier.TYPES.DarkTheme) { return commandService.keyboardShortcutForCommand(TOGGLE_DARK_MODE_COMMAND) } }, [commandService, uiFeature.featureIdentifier]) diff --git a/packages/web/src/javascripts/Components/SmartViewBuilder/CompoundPredicateBuilderController.ts b/packages/web/src/javascripts/Components/SmartViewBuilder/CompoundPredicateBuilderController.ts index 139e96f47..e6ad10b19 100644 --- a/packages/web/src/javascripts/Components/SmartViewBuilder/CompoundPredicateBuilderController.ts +++ b/packages/web/src/javascripts/Components/SmartViewBuilder/CompoundPredicateBuilderController.ts @@ -1,4 +1,4 @@ -import { FeatureIdentifier } from '@standardnotes/features' +import { NativeFeatureIdentifier } from '@standardnotes/features' import { NoteType, PredicateCompoundOperator, PredicateJsonForm } from '@standardnotes/snjs' import { makeObservable, observable, action } from 'mobx' import { PredicateKeypath, PredicateKeypathTypes } from './PredicateKeypaths' @@ -59,7 +59,7 @@ export class CompoundPredicateBuilderController { this.setPredicate(index, { value: Object.values(NoteType)[0] }) break case 'editorIdentifier': - this.setPredicate(index, { value: FeatureIdentifier.PlainEditor }) + this.setPredicate(index, { value: NativeFeatureIdentifier.TYPES.PlainEditor }) break case 'date': this.setPredicate(index, { value: '1.days.ago' }) diff --git a/packages/web/src/javascripts/Components/SuperEditor/SuperEditor.tsx b/packages/web/src/javascripts/Components/SuperEditor/SuperEditor.tsx index fc22f3cb8..f2f81b082 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/SuperEditor.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/SuperEditor.tsx @@ -7,7 +7,7 @@ import { isPayloadSourceRetrieved, PrefKey, PrefDefaults, - FeatureIdentifier, + NativeFeatureIdentifier, FeatureStatus, GetSuperNoteFeature, } from '@standardnotes/snjs' @@ -76,7 +76,12 @@ export const SuperEditor: FunctionComponent = ({ useEffect(() => { setFeatureStatus( - application.features.getFeatureStatus(FeatureIdentifier.SuperEditor, { inContextOfItem: note.current }), + application.features.getFeatureStatus( + NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SuperEditor).getValue(), + { + inContextOfItem: note.current, + }, + ), ) }, [application.features]) diff --git a/packages/web/src/javascripts/Controllers/FeaturesController.ts b/packages/web/src/javascripts/Controllers/FeaturesController.ts index 8797dc08c..2bc59a56c 100644 --- a/packages/web/src/javascripts/Controllers/FeaturesController.ts +++ b/packages/web/src/javascripts/Controllers/FeaturesController.ts @@ -4,7 +4,7 @@ import { PremiumFeatureModalType } from '@/Components/PremiumFeaturesModal/Premi import { destroyAllObjectProperties } from '@/Utils' import { ApplicationEvent, - FeatureIdentifier, + NativeFeatureIdentifier, FeatureStatus, InternalEventBusInterface, InternalEventInterface, @@ -100,19 +100,25 @@ export class FeaturesController extends AbstractViewController { } private isEntitledToFiles(): boolean { - const status = this.application.features.getFeatureStatus(FeatureIdentifier.Files) + const status = this.application.features.getFeatureStatus( + NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.Files).getValue(), + ) return status === FeatureStatus.Entitled } private isEntitledToFolders(): boolean { - const status = this.application.features.getFeatureStatus(FeatureIdentifier.TagNesting) + const status = this.application.features.getFeatureStatus( + NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.TagNesting).getValue(), + ) return status === FeatureStatus.Entitled } private isEntitledToSmartViews(): boolean { - const status = this.application.features.getFeatureStatus(FeatureIdentifier.SmartFilters) + const status = this.application.features.getFeatureStatus( + NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SmartFilters).getValue(), + ) return status === FeatureStatus.Entitled } diff --git a/packages/web/src/javascripts/Utils/DropdownItemsForEditors.ts b/packages/web/src/javascripts/Utils/DropdownItemsForEditors.ts index fcc619076..58d0aa9b1 100644 --- a/packages/web/src/javascripts/Utils/DropdownItemsForEditors.ts +++ b/packages/web/src/javascripts/Utils/DropdownItemsForEditors.ts @@ -1,11 +1,15 @@ -import { FeatureIdentifier } from '@standardnotes/snjs' -import { ComponentArea, FindNativeFeature, GetIframeAndNativeEditors } from '@standardnotes/features' +import { + ComponentArea, + FindNativeFeature, + GetIframeAndNativeEditors, + NativeFeatureIdentifier, +} from '@standardnotes/features' import { getIconAndTintForNoteType } from './Items/Icons/getIconAndTintForNoteType' import { DropdownItem } from '@/Components/Dropdown/DropdownItem' import { WebApplicationInterface } from '@standardnotes/ui-services' export type EditorOption = DropdownItem & { - value: FeatureIdentifier + value: string isLabs?: boolean } @@ -19,6 +23,7 @@ export function getDropdownItemsForAllEditors(application: WebApplicationInterfa return { label: editor.name, value: editor.identifier, + id: NativeFeatureIdentifier.create(editor.identifier).getValue(), ...(iconType ? { icon: iconType } : null), ...(tint ? { iconClassName: `text-accessory-tint-${tint}` } : null), } @@ -30,12 +35,11 @@ export function getDropdownItemsForAllEditors(application: WebApplicationInterfa .thirdPartyComponentsForArea(ComponentArea.Editor) .filter((component) => FindNativeFeature(component.identifier) === undefined) .map((editor): EditorOption => { - const identifier = editor.package_info.identifier const [iconType, tint] = getIconAndTintForNoteType(editor.noteType) return { label: editor.displayName, - value: identifier, + value: editor.uuid, ...(iconType ? { icon: iconType } : null), ...(tint ? { iconClassName: `text-accessory-tint-${tint}` } : null), } diff --git a/packages/web/src/javascripts/Utils/SortThemes.ts b/packages/web/src/javascripts/Utils/SortThemes.ts index 4eca5dc17..56e549ba3 100644 --- a/packages/web/src/javascripts/Utils/SortThemes.ts +++ b/packages/web/src/javascripts/Utils/SortThemes.ts @@ -1,7 +1,7 @@ -import { UIFeature, FeatureIdentifier, ThemeFeatureDescription } from '@standardnotes/snjs' +import { UIFeature, NativeFeatureIdentifier, ThemeFeatureDescription } from '@standardnotes/snjs' const isDarkModeTheme = (theme: UIFeature) => - theme.featureIdentifier === FeatureIdentifier.DarkTheme + theme.featureIdentifier === NativeFeatureIdentifier.TYPES.DarkTheme export const sortThemes = (a: UIFeature, b: UIFeature) => { const aIsLayerable = a.layerable diff --git a/packages/web/src/javascripts/Utils/createEditorMenuGroups.ts b/packages/web/src/javascripts/Utils/createEditorMenuGroups.ts index eed21cb3e..4c29a17d6 100644 --- a/packages/web/src/javascripts/Utils/createEditorMenuGroups.ts +++ b/packages/web/src/javascripts/Utils/createEditorMenuGroups.ts @@ -7,6 +7,8 @@ import { GetSuperNoteFeature, UIFeature, IframeComponentFeatureDescription, + Uuid, + NativeFeatureIdentifier, } from '@standardnotes/snjs' import { EditorMenuGroup } from '@/Components/NotesOptions/EditorMenuGroup' import { EditorMenuItem } from '@/Components/NotesOptions/EditorMenuItem' @@ -29,7 +31,9 @@ const insertNativeEditorsInMap = (map: NoteTypeToEditorRowsMap, application: Web const noteType = editorFeature.note_type map[noteType].push({ - isEntitled: application.features.getFeatureStatus(editorFeature.identifier) === FeatureStatus.Entitled, + isEntitled: + application.features.getFeatureStatus(NativeFeatureIdentifier.create(editorFeature.identifier).getValue()) === + FeatureStatus.Entitled, uiFeature: new UIFeature(editorFeature), }) } @@ -52,7 +56,7 @@ const insertInstalledComponentsInMap = (map: NoteTypeToEditorRowsMap, applicatio const editorItem: EditorMenuItem = { uiFeature: new UIFeature(editor), - isEntitled: application.features.getFeatureStatus(editor.identifier) === FeatureStatus.Entitled, + isEntitled: application.features.getFeatureStatus(Uuid.create(editor.uuid).getValue()) === FeatureStatus.Entitled, } map[noteType].push(editorItem)