mirror of
https://github.com/standardnotes/app
synced 2026-09-19 09:13:53 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0eb552ddc7 | ||
|
|
14bae5e895 | ||
|
|
1fef36d601 | ||
|
|
d268c02ab3 |
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.26.33](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.26.32](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.26.31](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api",
|
||||
"version": "1.26.31",
|
||||
"version": "1.26.33",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.1.117](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.116](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.115](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@standardnotes/clipper",
|
||||
"description": "Web clipper browser extension for Standard Notes",
|
||||
"version": "1.1.115",
|
||||
"version": "1.1.117",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build-mv2": "yarn clean && webpack --config ./webpack.config.prod.js",
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.108.48](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.108.47](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.108.46](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
|
||||
|
||||
### Reverts
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@standardnotes/desktop",
|
||||
"main": "./app/dist/index.js",
|
||||
"version": "3.108.46",
|
||||
"version": "3.108.48",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.21.56](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.21.55](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.21.54](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/encryption",
|
||||
"version": "1.21.54",
|
||||
"version": "1.21.56",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
+2
-2
@@ -14,9 +14,9 @@ export class GetPayloadAuthenticatedDataDetachedUseCase {
|
||||
execute(
|
||||
encrypted: EncryptedOutputParameters,
|
||||
): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData | undefined {
|
||||
const itemKeyComponents = deconstructEncryptedPayloadString(encrypted.enc_item_key)
|
||||
const contentKeyComponents = deconstructEncryptedPayloadString(encrypted.enc_item_key)
|
||||
|
||||
const authenticatedDataString = itemKeyComponents.authenticatedData
|
||||
const authenticatedDataString = contentKeyComponents.authenticatedData
|
||||
|
||||
const result = this.parseStringUseCase.execute<
|
||||
RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.59.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixes issue where lock screen would not use previously active theme ([#2372](https://github.com/standardnotes/app/issues/2372)) ([d268c02](https://github.com/standardnotes/app/commit/d268c02ab31beb5e2fd9e6547610f9a4dd61bed4))
|
||||
|
||||
## [1.59.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/features",
|
||||
"version": "1.59.10",
|
||||
"version": "1.59.11",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { FeatureIdentifier } from './../Feature/FeatureIdentifier'
|
||||
|
||||
type ThirdPartyIdentifier = string
|
||||
|
||||
export type EditorIdentifier = FeatureIdentifier | ThirdPartyIdentifier
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<EditorFeatureDescription | IframeComponentFeatureDescription>(
|
||||
identifier as FeatureIdentifier,
|
||||
)
|
||||
export function noteTypeForEditorIdentifier(identifier: string): NoteType {
|
||||
const feature = FindNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>(identifier)
|
||||
if (feature && feature.note_type) {
|
||||
return feature.note_type
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = []
|
||||
@@ -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<T extends AnyFeatureDescription>(identifier: FeatureIdentifier): T | undefined {
|
||||
export function FindNativeFeature<T extends AnyFeatureDescription>(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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Result, ValueObject } from '@standardnotes/domain-core'
|
||||
|
||||
export interface NativeFeatureIdentifierProps {
|
||||
value: string
|
||||
}
|
||||
|
||||
export class NativeFeatureIdentifier extends ValueObject<NativeFeatureIdentifierProps> {
|
||||
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<NativeFeatureIdentifier> {
|
||||
const isValidType = Object.values(this.TYPES).includes(type)
|
||||
if (!isValidType) {
|
||||
return Result.fail<NativeFeatureIdentifier>(`Invalid feature identifier: ${type}`)
|
||||
} else {
|
||||
return Result.ok<NativeFeatureIdentifier>(new NativeFeatureIdentifier({ value: type }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifier for standalone filesafe instance offered as legacy installable via extensions-server
|
||||
*/
|
||||
export const ExperimentalFeatures = []
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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: '',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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],
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.28.65](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.28.64](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.28.63](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/filepicker",
|
||||
"version": "1.28.63",
|
||||
"version": "1.28.65",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.16.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files
|
||||
|
||||
## [1.16.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files
|
||||
|
||||
## [1.16.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files",
|
||||
"version": "1.16.9",
|
||||
"version": "1.16.11",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.56.28](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.56.27](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.56.26](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/mobile",
|
||||
"version": "3.56.26",
|
||||
"version": "3.56.28",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.46.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.46.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixes issue where lock screen would not use previously active theme ([#2372](https://github.com/standardnotes/app/issues/2372)) ([d268c02](https://github.com/standardnotes/app/commit/d268c02ab31beb5e2fd9e6547610f9a4dd61bed4))
|
||||
|
||||
## [1.46.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/models",
|
||||
"version": "1.46.13",
|
||||
"version": "1.46.15",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection'
|
||||
import { ConflictDelta } from './Conflict'
|
||||
import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload'
|
||||
import { DeletedPayloadInterface, isDecryptedPayload, PayloadEmitSource } from '../../Abstract/Payload'
|
||||
import { FullyFormedPayloadInterface, isDecryptedPayload, PayloadEmitSource } from '../../Abstract/Payload'
|
||||
import { HistoryMap } from '../History'
|
||||
import { extendSyncDelta, SourcelessSyncDeltaEmit, SyncDeltaEmit } from './Abstract/DeltaEmit'
|
||||
import { DeltaInterface } from './Abstract/DeltaInterface'
|
||||
@@ -11,7 +10,7 @@ import { getIncrementedDirtyIndex } from '../DirtyCounter/DirtyCounter'
|
||||
export class DeltaFileImport implements DeltaInterface {
|
||||
constructor(
|
||||
readonly baseCollection: ImmutablePayloadCollection,
|
||||
private readonly applyPayloads: DecryptedPayloadInterface[],
|
||||
private readonly applyPayloads: FullyFormedPayloadInterface[],
|
||||
protected readonly historyMap: HistoryMap,
|
||||
) {}
|
||||
|
||||
@@ -31,10 +30,7 @@ export class DeltaFileImport implements DeltaInterface {
|
||||
return result
|
||||
}
|
||||
|
||||
private resolvePayload(
|
||||
payload: DecryptedPayloadInterface | DeletedPayloadInterface,
|
||||
currentResults: SyncDeltaEmit,
|
||||
): SourcelessSyncDeltaEmit {
|
||||
private resolvePayload(payload: FullyFormedPayloadInterface, currentResults: SyncDeltaEmit): SourcelessSyncDeltaEmit {
|
||||
/**
|
||||
* Check to see if we've already processed a payload for this id.
|
||||
* If so, that would be the latest value, and not what's in the base collection.
|
||||
|
||||
@@ -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<EditorFeatureDescription>
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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<F extends UIFeatureDescriptionTypes> implements UIFeatureInterface<F> {
|
||||
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<F extends UIFeatureDescriptionTypes> 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<F extends UIFeatureDescriptionTypes> 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<F extends UIFeatureDescriptionTypes> 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<F extends UIFeatureDescriptionTypes> 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<F extends UIFeatureDescriptionTypes> 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<F extends UIFeatureDescriptionTypes> 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<F extends UIFeatureDescriptionTypes> 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<F extends UIFeatureDescriptionTypes> 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<F extends UIFeatureDescriptionTypes> 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<F extends UIFeatureDescriptionTypes> 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
|
||||
|
||||
@@ -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<F extends UIFeatureDescriptionTypes> {
|
||||
item: ComponentInterface | F
|
||||
@@ -16,8 +17,8 @@ export interface UIFeatureInterface<F extends UIFeatureDescriptionTypes> {
|
||||
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
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { isValidUrl } from '@standardnotes/utils'
|
||||
import {
|
||||
FeatureIdentifier,
|
||||
ThirdPartyFeatureDescription,
|
||||
ComponentArea,
|
||||
ComponentFlag,
|
||||
@@ -175,7 +174,7 @@ export class SNComponent extends DecryptedItem<ComponentContent> 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
|
||||
}
|
||||
|
||||
|
||||
@@ -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<ComponentCont
|
||||
isExplicitlyDisabledForItem(uuid: string): boolean
|
||||
legacyIsDefaultEditor(): boolean
|
||||
|
||||
get identifier(): FeatureIdentifier
|
||||
get identifier(): string
|
||||
get noteType(): NoteType
|
||||
get displayName(): string
|
||||
get deprecationMessage(): string | undefined
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AppDataField } from './../../Abstract/Item/Types/AppDataField'
|
||||
import { FeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||
import { NoteType } from '@standardnotes/features'
|
||||
import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem'
|
||||
import { ItemInterface } from '../../Abstract/Item/Interfaces/ItemInterface'
|
||||
import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload'
|
||||
@@ -21,7 +21,7 @@ export class SNNote extends DecryptedItem<NoteContent> 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<NoteContent>) {
|
||||
super(payload)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<NoteContent> {
|
||||
this.mutableContent.noteType = noteType
|
||||
}
|
||||
|
||||
set editorIdentifier(identifier: FeatureIdentifier | string | undefined) {
|
||||
set editorIdentifier(identifier: string | undefined) {
|
||||
this.mutableContent.editorIdentifier = identifier
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import { FeatureIdentifier } from '@standardnotes/features'
|
||||
|
||||
type UuidString = string
|
||||
|
||||
export type AllComponentPreferences = Record<FeatureIdentifier | UuidString, ComponentPreferencesEntry>
|
||||
export type AllComponentPreferences = Record<string, ComponentPreferencesEntry>
|
||||
|
||||
export type ComponentPreferencesEntry = Record<string, unknown>
|
||||
|
||||
@@ -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]: '',
|
||||
|
||||
@@ -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<Record<SystemViewId, TagPreferences>>
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.4.400](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.399](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.398](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/releases",
|
||||
"version": "1.4.398",
|
||||
"version": "1.4.400",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "dist/releases.json",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.13.32](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/responses
|
||||
|
||||
## [1.13.31](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/responses
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/responses",
|
||||
"version": "1.13.31",
|
||||
"version": "1.13.32",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.63.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
## [1.63.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixes issue where lock screen would not use previously active theme ([#2372](https://github.com/standardnotes/app/issues/2372)) ([d268c02](https://github.com/standardnotes/app/commit/d268c02ab31beb5e2fd9e6547610f9a4dd61bed4))
|
||||
|
||||
## [1.63.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/services",
|
||||
"version": "1.63.11",
|
||||
"version": "1.63.13",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -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<ComponentFeatureDescription>): string | undefined
|
||||
@@ -31,12 +32,12 @@ export interface ComponentManagerInterface {
|
||||
setPermissionDialogUIHandler(handler: (dialog: PermissionDialog) => void): void
|
||||
|
||||
editorForNote(note: SNNote): UIFeature<EditorFeatureDescription | IframeComponentFeatureDescription>
|
||||
getDefaultEditorIdentifier(currentTag?: SNTag): EditorIdentifier
|
||||
getDefaultEditorIdentifier(currentTag?: SNTag): string
|
||||
|
||||
isThemeActive(theme: UIFeature<ThemeFeatureDescription>): boolean
|
||||
toggleTheme(theme: UIFeature<ThemeFeatureDescription>): Promise<void>
|
||||
getActiveThemes(): UIFeature<ThemeFeatureDescription>[]
|
||||
getActiveThemesIdentifiers(): string[]
|
||||
getActiveThemesIdentifiers(): { features: NativeFeatureIdentifier[]; uuids: Uuid[] }
|
||||
|
||||
isComponentActive(component: ComponentInterface): boolean
|
||||
toggleComponent(component: ComponentInterface): Promise<void>
|
||||
|
||||
@@ -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<IframeComponentFeatureDescription>
|
||||
|
||||
|
||||
@@ -89,7 +89,6 @@ export interface EncryptionProviderInterface {
|
||||
setNewRootKeyWrapper(wrappingKey: RootKeyInterface): Promise<void>
|
||||
|
||||
createNewItemsKeyWithRollback(): Promise<() => Promise<void>>
|
||||
reencryptApplicableItemsAfterUserRootKeyChange(): Promise<void>
|
||||
getSureDefaultItemsKey(): ItemsKeyInterface
|
||||
|
||||
createRandomizedKeySystemRootKey(dto: { systemIdentifier: KeySystemIdentifier }): KeySystemRootKeyInterface
|
||||
|
||||
@@ -240,10 +240,6 @@ export class EncryptionService
|
||||
return this.itemsEncryption.repersistAllItems()
|
||||
}
|
||||
|
||||
public async reencryptApplicableItemsAfterUserRootKeyChange(): Promise<void> {
|
||||
await this.rootKeyManager.reencryptApplicableItemsAfterUserRootKeyChange()
|
||||
}
|
||||
|
||||
public async createNewItemsKeyWithRollback(): Promise<() => Promise<void>> {
|
||||
return this._createNewItemsKeyWithRollback.execute()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { MutatorClientInterface } from './../../../Mutator/MutatorClientInterface'
|
||||
import { ItemManagerInterface } from './../../../Item/ItemManagerInterface'
|
||||
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
import { ContentTypesUsingRootKeyEncryption } from '@standardnotes/models'
|
||||
|
||||
/**
|
||||
* When the user root key changes, we must re-encrypt all relevant items with this new root key (by simply re-syncing).
|
||||
*/
|
||||
export class ReencryptTypeAItems implements UseCaseInterface<void> {
|
||||
constructor(private items: ItemManagerInterface, private mutator: MutatorClientInterface) {}
|
||||
|
||||
public async execute(): Promise<Result<void>> {
|
||||
const items = this.items.getItems(ContentTypesUsingRootKeyEncryption())
|
||||
if (items.length > 0) {
|
||||
/**
|
||||
* Do not call sync after marking dirty.
|
||||
* Re-encrypting items keys is called by consumers who have specific flows who
|
||||
* will sync on their own timing
|
||||
*/
|
||||
await this.mutator.setItemsDirty(items)
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
}
|
||||
@@ -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<ComponentInterface | undefined>
|
||||
}
|
||||
|
||||
@@ -76,9 +76,7 @@ export class KeySystemKeyManager
|
||||
}
|
||||
}
|
||||
|
||||
public getRootKeyFromStorageForVault(
|
||||
keySystemIdentifier: KeySystemIdentifier,
|
||||
): KeySystemRootKeyInterface | undefined {
|
||||
getRootKeyFromStorageForVault(keySystemIdentifier: KeySystemIdentifier): KeySystemRootKeyInterface | undefined {
|
||||
const payload = this.storage.getValue<DecryptedTransferPayload<KeySystemRootKeyContent>>(
|
||||
this.storageKeyForRootKey(keySystemIdentifier),
|
||||
)
|
||||
@@ -94,6 +92,10 @@ export class KeySystemKeyManager
|
||||
return key
|
||||
}
|
||||
|
||||
getMemCachedRootKey(systemIdentifier: KeySystemIdentifier): KeySystemRootKeyInterface {
|
||||
return this.rootKeyMemoryCache[systemIdentifier]
|
||||
}
|
||||
|
||||
private storageKeyForRootKey(systemIdentifier: KeySystemIdentifier): string {
|
||||
return `${RootKeyStorageKeyPrefix}${systemIdentifier}`
|
||||
}
|
||||
|
||||
@@ -9,18 +9,16 @@ import { ProtocolVersion, compareVersions } from '@standardnotes/common'
|
||||
import {
|
||||
BackupFile,
|
||||
BackupFileDecryptedContextualPayload,
|
||||
ComponentContent,
|
||||
CopyPayloadWithContentOverride,
|
||||
CreateDecryptedBackupFileContextPayload,
|
||||
CreateEncryptedBackupFileContextPayload,
|
||||
DecryptedItemInterface,
|
||||
DecryptedPayloadInterface,
|
||||
isDecryptedPayload,
|
||||
isEncryptedPayload,
|
||||
isEncryptedTransferPayload,
|
||||
} from '@standardnotes/models'
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
import { Challenge, ChallengePrompt, ChallengeReason, ChallengeValidation } from '../Challenge'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
|
||||
|
||||
const Strings = {
|
||||
@@ -57,44 +55,22 @@ export class ImportDataUseCase {
|
||||
* .affectedItems: Items that were either created or dirtied by this import
|
||||
* .errorCount: The number of items that were not imported due to failure to decrypt.
|
||||
*/
|
||||
|
||||
async execute(data: BackupFile, awaitSync = false): Promise<ImportDataReturnType> {
|
||||
if (data.version) {
|
||||
/**
|
||||
* Prior to 003 backup files did not have a version field so we cannot
|
||||
* stop importing if there is no backup file version, only if there is
|
||||
* an unsupported version.
|
||||
*/
|
||||
const version = data.version as ProtocolVersion
|
||||
|
||||
const supportedVersions = this.encryption.supportedVersions()
|
||||
if (!supportedVersions.includes(version)) {
|
||||
return { error: new ClientDisplayableError(Strings.UnsupportedBackupFileVersion) }
|
||||
}
|
||||
|
||||
const userVersion = this.encryption.getUserVersion()
|
||||
if (userVersion && compareVersions(version, userVersion) === 1) {
|
||||
/** File was made with a greater version than the user's account */
|
||||
return { error: new ClientDisplayableError(Strings.BackupFileMoreRecentThanAccount) }
|
||||
const result = this.validateVersion(data.version)
|
||||
if (result.isFailed()) {
|
||||
return { error: new ClientDisplayableError(result.getError()) }
|
||||
}
|
||||
}
|
||||
|
||||
let password: string | undefined
|
||||
|
||||
if (data.auth_params || data.keyParams) {
|
||||
/** Get import file password. */
|
||||
const challenge = new Challenge(
|
||||
[new ChallengePrompt(ChallengeValidation.None, Strings.FileAccountPassword, undefined, true)],
|
||||
ChallengeReason.DecryptEncryptedFile,
|
||||
true,
|
||||
)
|
||||
const passwordResponse = await this.challengeService.promptForChallengeResponse(challenge)
|
||||
if (passwordResponse == undefined) {
|
||||
/** Challenge was canceled */
|
||||
return { error: new ClientDisplayableError('Import aborted') }
|
||||
const passwordResult = await this.getFilePassword()
|
||||
if (passwordResult.isFailed()) {
|
||||
return { error: new ClientDisplayableError(passwordResult.getError()) }
|
||||
}
|
||||
this.challengeService.completeChallenge(challenge)
|
||||
password = passwordResponse?.values[0].value as string
|
||||
password = passwordResult.getValue()
|
||||
}
|
||||
|
||||
if (!(await this.protectionService.authorizeFileImport())) {
|
||||
@@ -110,31 +86,23 @@ export class ImportDataUseCase {
|
||||
})
|
||||
|
||||
const decryptedPayloadsOrError = await this._decryptBackFile.execute(data, password)
|
||||
|
||||
if (decryptedPayloadsOrError instanceof ClientDisplayableError) {
|
||||
return { error: decryptedPayloadsOrError }
|
||||
}
|
||||
|
||||
const validPayloads = decryptedPayloadsOrError.filter(isDecryptedPayload).map((payload) => {
|
||||
/* Don't want to activate any components during import process in
|
||||
* case of exceptions breaking up the import proccess */
|
||||
if (payload.content_type === ContentType.TYPES.Component && (payload.content as ComponentContent).active) {
|
||||
const typedContent = payload as DecryptedPayloadInterface<ComponentContent>
|
||||
return CopyPayloadWithContentOverride(typedContent, {
|
||||
active: false,
|
||||
})
|
||||
} else {
|
||||
return payload
|
||||
}
|
||||
const decryptedPayloads = decryptedPayloadsOrError.filter(isDecryptedPayload)
|
||||
const encryptedPayloads = decryptedPayloadsOrError.filter(isEncryptedPayload)
|
||||
const acceptableEncryptedPayloads = encryptedPayloads.filter((payload) => {
|
||||
return payload.key_system_identifier !== undefined
|
||||
})
|
||||
const importablePayloads = [...decryptedPayloads, ...acceptableEncryptedPayloads]
|
||||
|
||||
const affectedUuids = await this.payloadManager.importPayloads(
|
||||
validPayloads,
|
||||
importablePayloads,
|
||||
this.historyService.getHistoryMapCopy(),
|
||||
)
|
||||
|
||||
const promise = this.sync.sync()
|
||||
|
||||
if (awaitSync) {
|
||||
await promise
|
||||
}
|
||||
@@ -143,7 +111,42 @@ export class ImportDataUseCase {
|
||||
|
||||
return {
|
||||
affectedItems: affectedItems,
|
||||
errorCount: decryptedPayloadsOrError.length - validPayloads.length,
|
||||
errorCount: decryptedPayloadsOrError.length - importablePayloads.length,
|
||||
}
|
||||
}
|
||||
|
||||
private async getFilePassword(): Promise<Result<string>> {
|
||||
const challenge = new Challenge(
|
||||
[new ChallengePrompt(ChallengeValidation.None, Strings.FileAccountPassword, undefined, true)],
|
||||
ChallengeReason.DecryptEncryptedFile,
|
||||
true,
|
||||
)
|
||||
const passwordResponse = await this.challengeService.promptForChallengeResponse(challenge)
|
||||
if (passwordResponse == undefined) {
|
||||
/** Challenge was canceled */
|
||||
return Result.fail('Import aborted')
|
||||
}
|
||||
this.challengeService.completeChallenge(challenge)
|
||||
return Result.ok(passwordResponse?.values[0].value as string)
|
||||
}
|
||||
|
||||
/**
|
||||
* Prior to 003 backup files did not have a version field so we cannot
|
||||
* stop importing if there is no backup file version, only if there is
|
||||
* an unsupported version.
|
||||
*/
|
||||
private validateVersion(version: ProtocolVersion): Result<void> {
|
||||
const supportedVersions = this.encryption.supportedVersions()
|
||||
if (!supportedVersions.includes(version)) {
|
||||
return Result.fail(Strings.UnsupportedBackupFileVersion)
|
||||
}
|
||||
|
||||
const userVersion = this.encryption.getUserVersion()
|
||||
if (userVersion && compareVersions(version, userVersion) === 1) {
|
||||
/** File was made with a greater version than the user's account */
|
||||
return Result.fail(Strings.BackupFileMoreRecentThanAccount)
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
EncryptedPayloadInterface,
|
||||
FullyFormedPayloadInterface,
|
||||
PayloadEmitSource,
|
||||
DecryptedPayloadInterface,
|
||||
HistoryMap,
|
||||
} from '@standardnotes/models'
|
||||
import { IntegrityPayload } from '@standardnotes/responses'
|
||||
@@ -24,7 +23,7 @@ export interface PayloadManagerInterface {
|
||||
*/
|
||||
get nonDeletedItems(): FullyFormedPayloadInterface[]
|
||||
|
||||
importPayloads(payloads: DecryptedPayloadInterface[], historyMap: HistoryMap): Promise<string[]>
|
||||
importPayloads(payloads: FullyFormedPayloadInterface[], historyMap: HistoryMap): Promise<string[]>
|
||||
|
||||
removePayloadLocally(payload: FullyFormedPayloadInterface | FullyFormedPayloadInterface[]): void
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
EncryptionOperatorsInterface,
|
||||
} from '@standardnotes/encryption'
|
||||
import {
|
||||
ContentTypesUsingRootKeyEncryption,
|
||||
DecryptedPayload,
|
||||
DecryptedTransferPayload,
|
||||
EncryptedPayload,
|
||||
@@ -32,12 +31,11 @@ import { StorageValueModes } from '../Storage/StorageTypes'
|
||||
import { EncryptTypeAPayload } from '../Encryption/UseCase/TypeA/EncryptPayload'
|
||||
import { DecryptTypeAPayload } from '../Encryption/UseCase/TypeA/DecryptPayload'
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
|
||||
import { MutatorClientInterface } from '../Mutator/MutatorClientInterface'
|
||||
import { RootKeyManagerEvent } from './RootKeyManagerEvent'
|
||||
import { ValidatePasscodeResult } from './ValidatePasscodeResult'
|
||||
import { ValidateAccountPasswordResult } from './ValidateAccountPasswordResult'
|
||||
import { KeyMode } from './KeyMode'
|
||||
import { ReencryptTypeAItems } from '../Encryption/UseCase/TypeA/ReencryptTypeAItems'
|
||||
|
||||
export class RootKeyManager extends AbstractService<RootKeyManagerEvent> {
|
||||
private rootKey?: RootKeyInterface
|
||||
@@ -47,10 +45,9 @@ export class RootKeyManager extends AbstractService<RootKeyManagerEvent> {
|
||||
constructor(
|
||||
private device: DeviceInterface,
|
||||
private storage: StorageServiceInterface,
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
private operators: EncryptionOperatorsInterface,
|
||||
private identifier: ApplicationIdentifier,
|
||||
private _reencryptTypeAItems: ReencryptTypeAItems,
|
||||
eventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(eventBus)
|
||||
@@ -58,6 +55,12 @@ export class RootKeyManager extends AbstractService<RootKeyManagerEvent> {
|
||||
|
||||
override deinit() {
|
||||
super.deinit()
|
||||
;(this.device as unknown) = undefined
|
||||
;(this.storage as unknown) = undefined
|
||||
;(this.operators as unknown) = undefined
|
||||
;(this.identifier as unknown) = undefined
|
||||
;(this._reencryptTypeAItems as unknown) = undefined
|
||||
|
||||
this.rootKey = undefined
|
||||
this.memoizedRootKeyParams = undefined
|
||||
}
|
||||
@@ -307,7 +310,7 @@ export class RootKeyManager extends AbstractService<RootKeyManagerEvent> {
|
||||
if (this.keyMode === KeyMode.WrapperOnly || this.keyMode === KeyMode.RootKeyPlusWrapper) {
|
||||
if (this.keyMode === KeyMode.WrapperOnly) {
|
||||
this.setRootKeyInstance(wrappingKey)
|
||||
await this.reencryptApplicableItemsAfterUserRootKeyChange()
|
||||
await this._reencryptTypeAItems.execute()
|
||||
} else {
|
||||
await this.wrapAndPersistRootKey(wrappingKey)
|
||||
}
|
||||
@@ -473,19 +476,4 @@ export class RootKeyManager extends AbstractService<RootKeyManagerEvent> {
|
||||
keyParams: keyParams.getPortableValue(),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* When the root key changes, we must re-encrypt all relevant items with this new root key (by simply re-syncing).
|
||||
*/
|
||||
public async reencryptApplicableItemsAfterUserRootKeyChange(): Promise<void> {
|
||||
const items = this.items.getItems(ContentTypesUsingRootKeyEncryption())
|
||||
if (items.length > 0) {
|
||||
/**
|
||||
* Do not call sync after marking dirty.
|
||||
* Re-encrypting items keys is called by consumers who have specific flows who
|
||||
* will sync on their own timing
|
||||
*/
|
||||
await this.mutator.setItemsDirty(items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export enum SessionEvent {
|
||||
Restored = 'SessionRestored',
|
||||
Revoked = 'SessionRevoked',
|
||||
UserKeyPairChanged = 'UserKeyPairChanged',
|
||||
Restored = 'SessionEvent:SessionRestored',
|
||||
Revoked = 'SessionEvent:SessionRevoked',
|
||||
UserKeyPairChanged = 'SessionEvent:UserKeyPairChanged',
|
||||
}
|
||||
|
||||
@@ -18,8 +18,6 @@ export interface SessionsClientInterface {
|
||||
getWorkspaceDisplayIdentifier(): string
|
||||
populateSessionFromDemoShareToken(token: Base64String): Promise<void>
|
||||
|
||||
initializeFromDisk(): Promise<void>
|
||||
|
||||
getUser(): User | undefined
|
||||
isSignedIn(): boolean
|
||||
get userUuid(): string
|
||||
|
||||
@@ -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<SessionsClientInterface>
|
||||
sessions.isSignedIn = jest.fn().mockReturnValue(true)
|
||||
|
||||
storage = {} as jest.Mocked<StorageServiceInterface>
|
||||
|
||||
internalEventBus = {} as jest.Mocked<InternalEventBusInterface>
|
||||
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 () => {
|
||||
|
||||
@@ -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<void> {
|
||||
async fetchAvailableSubscriptions(): Promise<void> {
|
||||
try {
|
||||
const response = await this.subscriptionApiService.getAvailableSubscriptions()
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ReencryptTypeAItems } from './../Encryption/UseCase/TypeA/ReencryptTypeAItems'
|
||||
import { EncryptionProviderInterface } from './../Encryption/EncryptionProviderInterface'
|
||||
import { UserApiServiceInterface } from '@standardnotes/api'
|
||||
import { UserRequestType } from '@standardnotes/common'
|
||||
@@ -25,6 +26,7 @@ describe('UserService', () => {
|
||||
let challengeService: ChallengeServiceInterface
|
||||
let protectionService: ProtectionsClientInterface
|
||||
let userApiService: UserApiServiceInterface
|
||||
let reencryptTypeAItems: ReencryptTypeAItems
|
||||
let internalEventBus: InternalEventBusInterface
|
||||
|
||||
const createService = () =>
|
||||
@@ -38,6 +40,7 @@ describe('UserService', () => {
|
||||
challengeService,
|
||||
protectionService,
|
||||
userApiService,
|
||||
reencryptTypeAItems,
|
||||
internalEventBus,
|
||||
)
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ import { AccountEvent } from './AccountEvent'
|
||||
import { SignedInOrRegisteredEventPayload } from './SignedInOrRegisteredEventPayload'
|
||||
import { CredentialsChangeFunctionResponse } from './CredentialsChangeFunctionResponse'
|
||||
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
|
||||
import { ReencryptTypeAItems } from '../Encryption/UseCase/TypeA/ReencryptTypeAItems'
|
||||
|
||||
export class UserService
|
||||
extends AbstractService<AccountEvent, AccountEventData>
|
||||
@@ -49,33 +50,48 @@ export class UserService
|
||||
private readonly MINIMUM_PASSWORD_LENGTH = 8
|
||||
|
||||
constructor(
|
||||
private sessionManager: SessionsClientInterface,
|
||||
private sessions: SessionsClientInterface,
|
||||
private sync: SyncServiceInterface,
|
||||
private storageService: StorageServiceInterface,
|
||||
private itemManager: ItemManagerInterface,
|
||||
private encryptionService: EncryptionProviderInterface,
|
||||
private alertService: AlertService,
|
||||
private challengeService: ChallengeServiceInterface,
|
||||
private protectionService: ProtectionsClientInterface,
|
||||
private userApiService: UserApiServiceInterface,
|
||||
private storage: StorageServiceInterface,
|
||||
private items: ItemManagerInterface,
|
||||
private encryption: EncryptionProviderInterface,
|
||||
private alerts: AlertService,
|
||||
private challenges: ChallengeServiceInterface,
|
||||
private protections: ProtectionsClientInterface,
|
||||
private userApi: UserApiServiceInterface,
|
||||
private _reencryptTypeAItems: ReencryptTypeAItems,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
}
|
||||
|
||||
public override deinit(): void {
|
||||
super.deinit()
|
||||
;(this.sessions as unknown) = undefined
|
||||
;(this.sync as unknown) = undefined
|
||||
;(this.storage as unknown) = undefined
|
||||
;(this.items as unknown) = undefined
|
||||
;(this.encryption as unknown) = undefined
|
||||
;(this.alerts as unknown) = undefined
|
||||
;(this.challenges as unknown) = undefined
|
||||
;(this.protections as unknown) = undefined
|
||||
;(this.userApi as unknown) = undefined
|
||||
;(this._reencryptTypeAItems as unknown) = undefined
|
||||
}
|
||||
|
||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||
if (event.type === AccountEvent.SignedInOrRegistered) {
|
||||
const payload = (event.payload as AccountEventData).payload as SignedInOrRegisteredEventPayload
|
||||
this.sync.resetSyncState()
|
||||
|
||||
await this.storageService.setPersistencePolicy(
|
||||
await this.storage.setPersistencePolicy(
|
||||
payload.ephemeral ? StoragePersistencePolicies.Ephemeral : StoragePersistencePolicies.Default,
|
||||
)
|
||||
|
||||
if (payload.mergeLocal) {
|
||||
await this.sync.markAllItemsAsNeedingSyncAndPersist()
|
||||
} else {
|
||||
void this.itemManager.removeAllItemsFromMemory()
|
||||
void this.items.removeAllItemsFromMemory()
|
||||
await this.clearDatabase()
|
||||
}
|
||||
|
||||
@@ -88,37 +104,24 @@ export class UserService
|
||||
})
|
||||
.then(() => {
|
||||
if (!payload.awaitSync) {
|
||||
void this.encryptionService.decryptErroredPayloads()
|
||||
void this.encryption.decryptErroredPayloads()
|
||||
}
|
||||
})
|
||||
|
||||
if (payload.awaitSync) {
|
||||
await syncPromise
|
||||
|
||||
await this.encryptionService.decryptErroredPayloads()
|
||||
await this.encryption.decryptErroredPayloads()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override deinit(): void {
|
||||
super.deinit()
|
||||
;(this.sessionManager as unknown) = undefined
|
||||
;(this.sync as unknown) = undefined
|
||||
;(this.storageService as unknown) = undefined
|
||||
;(this.itemManager as unknown) = undefined
|
||||
;(this.encryptionService as unknown) = undefined
|
||||
;(this.alertService as unknown) = undefined
|
||||
;(this.challengeService as unknown) = undefined
|
||||
;(this.protectionService as unknown) = undefined
|
||||
;(this.userApiService as unknown) = undefined
|
||||
}
|
||||
|
||||
getUserUuid(): string {
|
||||
return this.sessionManager.userUuid
|
||||
return this.sessions.userUuid
|
||||
}
|
||||
|
||||
isSignedIn(): boolean {
|
||||
return this.sessionManager.isSignedIn()
|
||||
return this.sessions.isSignedIn()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,7 +134,7 @@ export class UserService
|
||||
ephemeral = false,
|
||||
mergeLocal = true,
|
||||
): Promise<UserRegistrationResponseBody> {
|
||||
if (this.encryptionService.hasAccount()) {
|
||||
if (this.encryption.hasAccount()) {
|
||||
throw Error('Tried to register when an account already exists.')
|
||||
}
|
||||
|
||||
@@ -143,7 +146,7 @@ export class UserService
|
||||
|
||||
try {
|
||||
this.lockSyncing()
|
||||
const response = await this.sessionManager.register(email, password, ephemeral)
|
||||
const response = await this.sessions.register(email, password, ephemeral)
|
||||
|
||||
await this.notifyEventSync(AccountEvent.SignedInOrRegistered, {
|
||||
payload: {
|
||||
@@ -177,7 +180,7 @@ export class UserService
|
||||
mergeLocal = true,
|
||||
awaitSync = false,
|
||||
): Promise<HttpResponse<SignInResponse>> {
|
||||
if (this.encryptionService.hasAccount()) {
|
||||
if (this.encryption.hasAccount()) {
|
||||
throw Error('Tried to sign in when an account already exists.')
|
||||
}
|
||||
|
||||
@@ -191,7 +194,7 @@ export class UserService
|
||||
/** Prevent a timed sync from occuring while signing in. */
|
||||
this.lockSyncing()
|
||||
|
||||
const { response } = await this.sessionManager.signIn(email, password, strict, ephemeral)
|
||||
const { response } = await this.sessions.signIn(email, password, strict, ephemeral)
|
||||
|
||||
if (!isErrorResponse(response)) {
|
||||
const notifyingFunction = awaitSync ? this.notifyEventSync.bind(this) : this.notifyEvent.bind(this)
|
||||
@@ -218,7 +221,7 @@ export class UserService
|
||||
message?: string
|
||||
}> {
|
||||
if (
|
||||
!(await this.protectionService.authorizeAction(ChallengeReason.DeleteAccount, {
|
||||
!(await this.protections.authorizeAction(ChallengeReason.DeleteAccount, {
|
||||
fallBackToAccountPassword: true,
|
||||
requireAccountPassword: true,
|
||||
forcePrompt: false,
|
||||
@@ -230,8 +233,8 @@ export class UserService
|
||||
}
|
||||
}
|
||||
|
||||
const uuid = this.sessionManager.getSureUser().uuid
|
||||
const response = await this.userApiService.deleteAccount(uuid)
|
||||
const uuid = this.sessions.getSureUser().uuid
|
||||
const response = await this.userApi.deleteAccount(uuid)
|
||||
if (isErrorResponse(response)) {
|
||||
return {
|
||||
error: true,
|
||||
@@ -241,7 +244,7 @@ export class UserService
|
||||
|
||||
await this.signOut(true)
|
||||
|
||||
void this.alertService.alert(InfoStrings.AccountDeleted)
|
||||
void this.alerts.alert(InfoStrings.AccountDeleted)
|
||||
|
||||
return {
|
||||
error: false,
|
||||
@@ -249,9 +252,9 @@ export class UserService
|
||||
}
|
||||
|
||||
async submitUserRequest(requestType: UserRequestType): Promise<boolean> {
|
||||
const userUuid = this.sessionManager.getSureUser().uuid
|
||||
const userUuid = this.sessions.getSureUser().uuid
|
||||
try {
|
||||
const result = await this.userApiService.submitUserRequest({
|
||||
const result = await this.userApi.submitUserRequest({
|
||||
userUuid,
|
||||
requestType,
|
||||
})
|
||||
@@ -274,11 +277,7 @@ export class UserService
|
||||
public async correctiveSignIn(rootKey: SNRootKey): Promise<HttpResponse<SignInResponse>> {
|
||||
this.lockSyncing()
|
||||
|
||||
const response = await this.sessionManager.bypassChecksAndSignInWithRootKey(
|
||||
rootKey.keyParams.identifier,
|
||||
rootKey,
|
||||
false,
|
||||
)
|
||||
const response = await this.sessions.bypassChecksAndSignInWithRootKey(rootKey.keyParams.identifier, rootKey, false)
|
||||
|
||||
if (!isErrorResponse(response)) {
|
||||
await this.notifyEvent(AccountEvent.SignedInOrRegistered, {
|
||||
@@ -313,16 +312,16 @@ export class UserService
|
||||
}): Promise<CredentialsChangeFunctionResponse> {
|
||||
const result = await this.performCredentialsChange(parameters)
|
||||
if (result.error) {
|
||||
void this.alertService.alert(result.error.message)
|
||||
void this.alerts.alert(result.error.message)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public async signOut(force = false, source = DeinitSource.SignOut): Promise<void> {
|
||||
const performSignOut = async () => {
|
||||
await this.sessionManager.signOut()
|
||||
await this.encryptionService.deleteWorkspaceSpecificKeyStateFromDevice()
|
||||
await this.storageService.clearAllData()
|
||||
await this.sessions.signOut()
|
||||
await this.encryption.deleteWorkspaceSpecificKeyStateFromDevice()
|
||||
await this.storage.clearAllData()
|
||||
await this.notifyEvent(AccountEvent.SignedOut, { payload: { source } })
|
||||
}
|
||||
|
||||
@@ -332,10 +331,10 @@ export class UserService
|
||||
return
|
||||
}
|
||||
|
||||
const dirtyItems = this.itemManager.getDirtyItems()
|
||||
const dirtyItems = this.items.getDirtyItems()
|
||||
if (dirtyItems.length > 0) {
|
||||
const singular = dirtyItems.length === 1
|
||||
const didConfirm = await this.alertService.confirm(
|
||||
const didConfirm = await this.alerts.confirm(
|
||||
`There ${singular ? 'is' : 'are'} ${dirtyItems.length} ${
|
||||
singular ? 'item' : 'items'
|
||||
} with unsynced changes. If you sign out, these changes will be lost forever. Are you sure you want to sign out?`,
|
||||
@@ -353,7 +352,7 @@ export class UserService
|
||||
canceled?: true
|
||||
error?: { message: string }
|
||||
}> {
|
||||
if (!this.sessionManager.isUserMissingKeyPair()) {
|
||||
if (!this.sessions.isUserMissingKeyPair()) {
|
||||
throw Error('Cannot update account with first time keypair if user already has a keypair')
|
||||
}
|
||||
|
||||
@@ -367,8 +366,8 @@ export class UserService
|
||||
canceled?: true
|
||||
error?: { message: string }
|
||||
}> {
|
||||
const hasPasscode = this.encryptionService.hasPasscode()
|
||||
const hasAccount = this.encryptionService.hasAccount()
|
||||
const hasPasscode = this.encryption.hasPasscode()
|
||||
const hasAccount = this.encryption.hasAccount()
|
||||
const prompts = []
|
||||
if (hasPasscode) {
|
||||
prompts.push(
|
||||
@@ -389,11 +388,11 @@ export class UserService
|
||||
)
|
||||
}
|
||||
const challenge = new Challenge(prompts, ChallengeReason.ProtocolUpgrade, true)
|
||||
const response = await this.challengeService.promptForChallengeResponse(challenge)
|
||||
const response = await this.challenges.promptForChallengeResponse(challenge)
|
||||
if (!response) {
|
||||
return { canceled: true }
|
||||
}
|
||||
const dismissBlockingDialog = await this.alertService.blockingDialog(
|
||||
const dismissBlockingDialog = await this.alerts.blockingDialog(
|
||||
Messages.DO_NOT_CLOSE_APPLICATION,
|
||||
Messages.UPGRADING_ENCRYPTION,
|
||||
)
|
||||
@@ -436,11 +435,11 @@ export class UserService
|
||||
if (passcode.length < this.MINIMUM_PASSCODE_LENGTH) {
|
||||
return false
|
||||
}
|
||||
if (!(await this.protectionService.authorizeAddingPasscode())) {
|
||||
if (!(await this.protections.authorizeAddingPasscode())) {
|
||||
return false
|
||||
}
|
||||
|
||||
const dismissBlockingDialog = await this.alertService.blockingDialog(
|
||||
const dismissBlockingDialog = await this.alerts.blockingDialog(
|
||||
Messages.DO_NOT_CLOSE_APPLICATION,
|
||||
Messages.SETTING_PASSCODE,
|
||||
)
|
||||
@@ -453,11 +452,11 @@ export class UserService
|
||||
}
|
||||
|
||||
public async removePasscode(): Promise<boolean> {
|
||||
if (!(await this.protectionService.authorizeRemovingPasscode())) {
|
||||
if (!(await this.protections.authorizeRemovingPasscode())) {
|
||||
return false
|
||||
}
|
||||
|
||||
const dismissBlockingDialog = await this.alertService.blockingDialog(
|
||||
const dismissBlockingDialog = await this.alerts.blockingDialog(
|
||||
Messages.DO_NOT_CLOSE_APPLICATION,
|
||||
Messages.REMOVING_PASSCODE,
|
||||
)
|
||||
@@ -479,11 +478,11 @@ export class UserService
|
||||
if (newPasscode.length < this.MINIMUM_PASSCODE_LENGTH) {
|
||||
return false
|
||||
}
|
||||
if (!(await this.protectionService.authorizeChangingPasscode())) {
|
||||
if (!(await this.protections.authorizeChangingPasscode())) {
|
||||
return false
|
||||
}
|
||||
|
||||
const dismissBlockingDialog = await this.alertService.blockingDialog(
|
||||
const dismissBlockingDialog = await this.alerts.blockingDialog(
|
||||
Messages.DO_NOT_CLOSE_APPLICATION,
|
||||
origination === KeyParamsOrigination.ProtocolUpgrade
|
||||
? Messages.ProtocolUpgradeStrings.UpgradingPasscode
|
||||
@@ -499,7 +498,7 @@ export class UserService
|
||||
}
|
||||
|
||||
public async populateSessionFromDemoShareToken(token: Base64String): Promise<void> {
|
||||
await this.sessionManager.populateSessionFromDemoShareToken(token)
|
||||
await this.sessions.populateSessionFromDemoShareToken(token)
|
||||
await this.notifyEvent(AccountEvent.SignedInOrRegistered, {
|
||||
payload: {
|
||||
ephemeral: false,
|
||||
@@ -512,14 +511,14 @@ export class UserService
|
||||
|
||||
private async setPasscodeWithoutWarning(passcode: string, origination: KeyParamsOrigination) {
|
||||
const identifier = UuidGenerator.GenerateUuid()
|
||||
const key = await this.encryptionService.createRootKey(identifier, passcode, origination)
|
||||
await this.encryptionService.setNewRootKeyWrapper(key)
|
||||
const key = await this.encryption.createRootKey(identifier, passcode, origination)
|
||||
await this.encryption.setNewRootKeyWrapper(key)
|
||||
await this.rewriteItemsKeys()
|
||||
await this.sync.sync()
|
||||
}
|
||||
|
||||
private async removePasscodeWithoutWarning() {
|
||||
await this.encryptionService.removePasscode()
|
||||
await this.encryption.removePasscode()
|
||||
await this.rewriteItemsKeys()
|
||||
}
|
||||
|
||||
@@ -532,9 +531,9 @@ export class UserService
|
||||
* https://github.com/standardnotes/desktop/issues/131
|
||||
*/
|
||||
private async rewriteItemsKeys(): Promise<void> {
|
||||
const itemsKeys = this.itemManager.getDisplayableItemsKeys()
|
||||
const itemsKeys = this.items.getDisplayableItemsKeys()
|
||||
const payloads = itemsKeys.map((key) => key.payloadRepresentation())
|
||||
await this.storageService.deletePayloads(payloads)
|
||||
await this.storage.deletePayloads(payloads)
|
||||
await this.sync.persistPayloads(payloads)
|
||||
}
|
||||
|
||||
@@ -547,7 +546,7 @@ export class UserService
|
||||
}
|
||||
|
||||
private clearDatabase(): Promise<void> {
|
||||
return this.storageService.clearAllPayloads()
|
||||
return this.storage.clearAllPayloads()
|
||||
}
|
||||
|
||||
private async performCredentialsChange(parameters: {
|
||||
@@ -558,7 +557,7 @@ export class UserService
|
||||
newPassword?: string
|
||||
passcode?: string
|
||||
}): Promise<CredentialsChangeFunctionResponse> {
|
||||
const { wrappingKey, canceled } = await this.challengeService.getWrappingKeyIfApplicable(parameters.passcode)
|
||||
const { wrappingKey, canceled } = await this.challenges.getWrappingKeyIfApplicable(parameters.passcode)
|
||||
|
||||
if (canceled) {
|
||||
return { error: Error(Messages.CredentialsChangeStrings.PasscodeRequired) }
|
||||
@@ -572,14 +571,14 @@ export class UserService
|
||||
}
|
||||
}
|
||||
|
||||
const accountPasswordValidation = await this.encryptionService.validateAccountPassword(parameters.currentPassword)
|
||||
const accountPasswordValidation = await this.encryption.validateAccountPassword(parameters.currentPassword)
|
||||
if (!accountPasswordValidation.valid) {
|
||||
return {
|
||||
error: Error(Messages.INVALID_PASSWORD),
|
||||
}
|
||||
}
|
||||
|
||||
const user = this.sessionManager.getUser() as User
|
||||
const user = this.sessions.getUser() as User
|
||||
const currentEmail = user.email
|
||||
const { currentRootKey, newRootKey } = await this.recomputeRootKeysForCredentialChange({
|
||||
currentPassword: parameters.currentPassword,
|
||||
@@ -591,7 +590,7 @@ export class UserService
|
||||
|
||||
this.lockSyncing()
|
||||
|
||||
const { response } = await this.sessionManager.changeCredentials({
|
||||
const { response } = await this.sessions.changeCredentials({
|
||||
currentServerPassword: currentRootKey.serverPassword as string,
|
||||
newRootKey: newRootKey,
|
||||
wrappingKey,
|
||||
@@ -604,20 +603,20 @@ export class UserService
|
||||
return { error: Error(response.data.error?.message) }
|
||||
}
|
||||
|
||||
const rollback = await this.encryptionService.createNewItemsKeyWithRollback()
|
||||
await this.encryptionService.reencryptApplicableItemsAfterUserRootKeyChange()
|
||||
const rollback = await this.encryption.createNewItemsKeyWithRollback()
|
||||
await this._reencryptTypeAItems.execute()
|
||||
await this.sync.sync({ awaitAll: true })
|
||||
|
||||
const defaultItemsKey = this.encryptionService.getSureDefaultItemsKey()
|
||||
const defaultItemsKey = this.encryption.getSureDefaultItemsKey()
|
||||
const itemsKeyWasSynced = !defaultItemsKey.neverSynced
|
||||
|
||||
if (!itemsKeyWasSynced) {
|
||||
await this.sessionManager.changeCredentials({
|
||||
await this.sessions.changeCredentials({
|
||||
currentServerPassword: newRootKey.serverPassword as string,
|
||||
newRootKey: currentRootKey,
|
||||
wrappingKey,
|
||||
})
|
||||
await this.encryptionService.reencryptApplicableItemsAfterUserRootKeyChange()
|
||||
await this._reencryptTypeAItems.execute()
|
||||
await rollback()
|
||||
await this.sync.sync({ awaitAll: true })
|
||||
|
||||
@@ -634,11 +633,11 @@ export class UserService
|
||||
newEmail?: string
|
||||
newPassword?: string
|
||||
}): Promise<{ currentRootKey: SNRootKey; newRootKey: SNRootKey }> {
|
||||
const currentRootKey = await this.encryptionService.computeRootKey(
|
||||
const currentRootKey = await this.encryption.computeRootKey(
|
||||
parameters.currentPassword,
|
||||
(await this.encryptionService.getRootKeyParams()) as SNRootKeyParams,
|
||||
this.encryption.getRootKeyParams() as SNRootKeyParams,
|
||||
)
|
||||
const newRootKey = await this.encryptionService.createRootKey(
|
||||
const newRootKey = await this.encryption.createRootKey(
|
||||
parameters.newEmail ?? parameters.currentEmail,
|
||||
parameters.newPassword ?? parameters.currentPassword,
|
||||
parameters.origination,
|
||||
|
||||
@@ -75,6 +75,7 @@ export * from './Encryption/UseCase/TypeA/DecryptPayload'
|
||||
export * from './Encryption/UseCase/TypeA/DecryptPayloadWithKeyLookup'
|
||||
export * from './Encryption/UseCase/TypeA/EncryptPayload'
|
||||
export * from './Encryption/UseCase/TypeA/EncryptPayloadWithKeyLookup'
|
||||
export * from './Encryption/UseCase/TypeA/ReencryptTypeAItems'
|
||||
export * from './Event/ApplicationEvent'
|
||||
export * from './Event/ApplicationEventCallback'
|
||||
export * from './Event/ApplicationStageChangedEventPayload'
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.202.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
## [2.202.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixes issue where lock screen would not use previously active theme ([#2372](https://github.com/standardnotes/app/issues/2372)) ([d268c02](https://github.com/standardnotes/app/commit/d268c02ab31beb5e2fd9e6547610f9a4dd61bed4))
|
||||
|
||||
## [2.202.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -119,6 +119,7 @@ import {
|
||||
DeleteContact,
|
||||
VaultLockService,
|
||||
RemoveItemsFromMemory,
|
||||
ReencryptTypeAItems,
|
||||
} from '@standardnotes/services'
|
||||
import { ItemManager } from '../../Services/Items/ItemManager'
|
||||
import { PayloadManager } from '../../Services/Payloads/PayloadManager'
|
||||
@@ -202,6 +203,10 @@ export class Dependencies {
|
||||
}
|
||||
|
||||
private registerUseCaseMakers() {
|
||||
this.factory.set(TYPES.ReencryptTypeAItems, () => {
|
||||
return new ReencryptTypeAItems(this.get(TYPES.ItemManager), this.get(TYPES.MutatorService))
|
||||
})
|
||||
|
||||
this.factory.set(TYPES.ImportDataUseCase, () => {
|
||||
return new ImportDataUseCase(
|
||||
this.get(TYPES.ItemManager),
|
||||
@@ -616,10 +621,9 @@ export class Dependencies {
|
||||
return new RootKeyManager(
|
||||
this.get(TYPES.DeviceInterface),
|
||||
this.get(TYPES.DiskStorageService),
|
||||
this.get(TYPES.ItemManager),
|
||||
this.get(TYPES.MutatorService),
|
||||
this.get(TYPES.EncryptionOperators),
|
||||
this.options.identifier,
|
||||
this.get(TYPES.ReencryptTypeAItems),
|
||||
this.get(TYPES.InternalEventBus),
|
||||
)
|
||||
})
|
||||
@@ -1086,6 +1090,7 @@ export class Dependencies {
|
||||
this.get(TYPES.ChallengeService),
|
||||
this.get(TYPES.ProtectionService),
|
||||
this.get(TYPES.UserApiService),
|
||||
this.get(TYPES.ReencryptTypeAItems),
|
||||
this.get(TYPES.InternalEventBus),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -151,6 +151,7 @@ export const TYPES = {
|
||||
DecryptBackupFile: Symbol.for('DecryptBackupFile'),
|
||||
IsVaultOwner: Symbol.for('IsVaultOwner'),
|
||||
RemoveItemsFromMemory: Symbol.for('RemoveItemsFromMemory'),
|
||||
ReencryptTypeAItems: Symbol.for('ReencryptTypeAItems'),
|
||||
|
||||
// Mappers
|
||||
SessionStorageMapper: Symbol.for('SessionStorageMapper'),
|
||||
|
||||
@@ -14,7 +14,6 @@ export class Migration2_202_1 extends Migration {
|
||||
this.registerStageHandler(ApplicationStage.FullSyncCompleted_13, async () => {
|
||||
await this.migrateComponentDataToUserPreferences()
|
||||
await this.migrateActiveComponentsToUserPreferences()
|
||||
await this.deleteComponentsWhichAreNativeFeatures()
|
||||
|
||||
this.markDone()
|
||||
})
|
||||
@@ -70,29 +69,4 @@ export class Migration2_202_1 extends Migration {
|
||||
await this.services.preferences.setValueDetached(PrefKey.ActiveThemes, Uuids(activeThemes))
|
||||
await this.services.preferences.setValueDetached(PrefKey.ActiveComponents, Uuids(activeComponents))
|
||||
}
|
||||
|
||||
private async deleteComponentsWhichAreNativeFeatures(): Promise<void> {
|
||||
const componentsToDelete = [
|
||||
...this.services.itemManager.getItems<ComponentInterface>(ContentType.TYPES.Component),
|
||||
...this.services.itemManager.getItems<ComponentInterface>(ContentType.TYPES.Theme),
|
||||
].filter((candidate) => {
|
||||
const nativeFeature = FindNativeFeature(candidate.identifier)
|
||||
if (!nativeFeature) {
|
||||
return false
|
||||
}
|
||||
|
||||
const isDeprecatedAndThusShouldNotDeleteComponentSinceUserHasItRetained = nativeFeature.deprecated
|
||||
if (isDeprecatedAndThusShouldNotDeleteComponentSinceUserHasItRetained) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
if (componentsToDelete.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
await this.services.mutator.setItemsToBeDeleted(componentsToDelete)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<ThemeFeatureDescription>[] {
|
||||
const activeThemesIdentifiers = this.getActiveThemesIdentifiers()
|
||||
const { features, uuids } = this.getActiveThemesIdentifiers()
|
||||
|
||||
const thirdPartyThemes = this.items.findItems<ThemeInterface>(activeThemesIdentifiers).map((item) => {
|
||||
return new UIFeature<ThemeFeatureDescription>(item)
|
||||
})
|
||||
const thirdPartyThemes = uuids
|
||||
.map((uuid) => {
|
||||
const component = this.items.findItem<ComponentInterface>(uuid.value)
|
||||
if (component) {
|
||||
return new UIFeature<ThemeFeatureDescription>(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<void> {
|
||||
@@ -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<ThemeFeatureDescription>): Promise<void> {
|
||||
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<ThemeFeatureDescription>): Promise<void> {
|
||||
await this.preferences.setValue(PrefKey.ActiveThemes, [theme.uniqueIdentifier])
|
||||
await this.preferences.setValue(PrefKey.ActiveThemes, [theme.uniqueIdentifier.value])
|
||||
}
|
||||
|
||||
async removeActiveTheme(theme: UIFeature<ThemeFeatureDescription>): Promise<void> {
|
||||
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<ThemeFeatureDescription>): 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<void> {
|
||||
|
||||
@@ -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<string, unknown> {
|
||||
const globalComponentData = item.getDomainData(ComponentDataDomain) || {}
|
||||
const thisComponentData = globalComponentData[this.componentUniqueIdentifier] || {}
|
||||
const thisComponentData = globalComponentData[this.componentUniqueIdentifier.value] || {}
|
||||
return thisComponentData as Record<string, unknown>
|
||||
}
|
||||
|
||||
@@ -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<Record<string, unknown>>(
|
||||
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<Record<string, unknown>>(
|
||||
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 = <ComponentPreferencesEntry | undefined>message.data.componentData
|
||||
|
||||
@@ -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<T> = { -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,
|
||||
])
|
||||
|
||||
/**
|
||||
|
||||
+34
-14
@@ -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 = <F extends UIFeatureDescriptionTypes>(identifier: FeatureIdentifier) => {
|
||||
const nativeFeatureAsUIFeature = <F extends UIFeatureDescriptionTypes>(identifier: string) => {
|
||||
return new UIFeature(FindNativeFeature<F>(identifier)!)
|
||||
}
|
||||
|
||||
@@ -19,57 +19,77 @@ describe('editor change alert', () => {
|
||||
})
|
||||
|
||||
it('should not require alert switching from plain editor', () => {
|
||||
const component = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.MarkdownProEditor)!
|
||||
const component = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
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<IframeComponentFeatureDescription>(FeatureIdentifier.MarkdownProEditor)!
|
||||
const component = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
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<IframeComponentFeatureDescription>(FeatureIdentifier.PlusEditor)!
|
||||
const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
NativeFeatureIdentifier.TYPES.PlusEditor,
|
||||
)!
|
||||
const markdownEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
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<IframeComponentFeatureDescription>(FeatureIdentifier.PlusEditor)!
|
||||
const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
NativeFeatureIdentifier.TYPES.PlusEditor,
|
||||
)!
|
||||
const markdownEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
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<IframeComponentFeatureDescription>(FeatureIdentifier.PlusEditor)!
|
||||
const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
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<IframeComponentFeatureDescription>(FeatureIdentifier.PlusEditor)!
|
||||
const customEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.TokenVaultEditor)
|
||||
const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
NativeFeatureIdentifier.TYPES.PlusEditor,
|
||||
)!
|
||||
const customEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
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<IframeComponentFeatureDescription>(FeatureIdentifier.PlusEditor)!
|
||||
const customEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.TokenVaultEditor)
|
||||
const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
NativeFeatureIdentifier.TYPES.PlusEditor,
|
||||
)!
|
||||
const customEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
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<IframeComponentFeatureDescription>(FeatureIdentifier.TokenVaultEditor)
|
||||
const customEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
NativeFeatureIdentifier.TYPES.TokenVaultEditor,
|
||||
)
|
||||
const requiresAlert = usecase.execute(customEditor, customEditor)
|
||||
expect(requiresAlert).toBe(true)
|
||||
})
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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<EditorFeatureDescription | IframeComponentFeatureDescription> | undefined {
|
||||
const nativeFeature = FindNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>(
|
||||
identifier as FeatureIdentifier,
|
||||
)
|
||||
const nativeFeature = FindNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>(identifier)
|
||||
if (nativeFeature) {
|
||||
return new UIFeature(nativeFeature)
|
||||
}
|
||||
|
||||
+8
-8
@@ -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<SNTag>
|
||||
|
||||
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<SNComponent>
|
||||
|
||||
@@ -55,6 +55,6 @@ describe('getDefaultEditorIdentifier', () => {
|
||||
|
||||
const editorIdentifier = usecase.execute().getValue()
|
||||
|
||||
expect(editorIdentifier).toEqual(FeatureIdentifier.MarkdownProEditor)
|
||||
expect(editorIdentifier).toEqual(NativeFeatureIdentifier.TYPES.MarkdownProEditor)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<EditorIdentifier> {
|
||||
export class GetDefaultEditorIdentifier implements SyncUseCaseInterface<string> {
|
||||
constructor(private preferences: PreferenceServiceInterface, private items: ItemManagerInterface) {}
|
||||
|
||||
execute(currentTag?: SNTag): Result<EditorIdentifier> {
|
||||
execute(currentTag?: SNTag): Result<string> {
|
||||
if (currentTag) {
|
||||
const editorIdentifier = currentTag?.preferences?.editorIdentifier
|
||||
if (editorIdentifier) {
|
||||
@@ -25,7 +25,7 @@ export class GetDefaultEditorIdentifier implements SyncUseCaseInterface<EditorId
|
||||
return Result.ok(matchingEditor.identifier)
|
||||
}
|
||||
|
||||
return Result.ok(FeatureIdentifier.PlainEditor)
|
||||
return Result.ok(NativeFeatureIdentifier.TYPES.PlainEditor)
|
||||
}
|
||||
|
||||
thirdPartyComponentsForArea(area: ComponentArea): ComponentInterface[] {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import {
|
||||
FeatureIdentifier,
|
||||
NativeFeatureIdentifier,
|
||||
FindNativeFeature,
|
||||
IframeComponentFeatureDescription,
|
||||
UIFeatureDescriptionTypes,
|
||||
@@ -21,7 +21,7 @@ import { GetFeatureUrl } from './GetFeatureUrl'
|
||||
|
||||
const desktopExtHost = 'http://localhost:123'
|
||||
|
||||
const nativeFeatureAsUIFeature = <F extends UIFeatureDescriptionTypes>(identifier: FeatureIdentifier) => {
|
||||
const nativeFeatureAsUIFeature = <F extends UIFeatureDescriptionTypes>(identifier: string) => {
|
||||
return new UIFeature(FindNativeFeature<F>(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<ComponentPackageInfo>,
|
||||
@@ -75,7 +75,9 @@ describe('GetFeatureUrl', () => {
|
||||
})
|
||||
|
||||
it('returns native path for native component', () => {
|
||||
const feature = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.MarkdownProEditor)!
|
||||
const feature = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
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<IframeComponentFeatureDescription>(
|
||||
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<IframeComponentFeatureDescription>(FeatureIdentifier.MarkdownProEditor)
|
||||
const feature = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||
NativeFeatureIdentifier.TYPES.MarkdownProEditor,
|
||||
)
|
||||
const url = usecase.execute(feature)
|
||||
expect(url).toEqual(
|
||||
`http://localhost/components/assets/${feature.featureIdentifier}/${feature.asFeatureDescription.index_path}`,
|
||||
|
||||
+13
-10
@@ -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 = <F extends UIFeatureDescriptionTypes>(identifier: FeatureIdentifier) => {
|
||||
const nativeFeatureAsUIFeature = <F extends UIFeatureDescriptionTypes>(identifier: string) => {
|
||||
return new UIFeature(FindNativeFeature<F>(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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<ComponentFeatureDescription> | undefined {
|
||||
const nativeFeature = FindNativeFeature<ComponentFeatureDescription>(identifier as FeatureIdentifier)
|
||||
const nativeFeature = FindNativeFeature<ComponentFeatureDescription>(identifier)
|
||||
if (nativeFeature) {
|
||||
return new UIFeature(nativeFeature)
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import { ItemInterface, SNFeatureRepo } from '@standardnotes/models'
|
||||
import { SyncService } from '../Sync/SyncService'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { FeaturesService } from '@Lib/Services/Features'
|
||||
import { RoleName, ContentType } from '@standardnotes/domain-core'
|
||||
import { FeatureIdentifier, GetFeatures } from '@standardnotes/features'
|
||||
import { RoleName, ContentType, Uuid } from '@standardnotes/domain-core'
|
||||
import { NativeFeatureIdentifier, GetFeatures } from '@standardnotes/features'
|
||||
import { WebSocketsService } from '../Api/WebsocketsService'
|
||||
import { SettingsService } from '../Settings'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
@@ -44,6 +44,7 @@ describe('FeaturesService', () => {
|
||||
let roles: string[]
|
||||
let items: ItemInterface[]
|
||||
let internalEventBus: InternalEventBusInterface
|
||||
let featureService: FeaturesService
|
||||
|
||||
const createService = () => {
|
||||
return new FeaturesService(
|
||||
@@ -118,23 +119,81 @@ describe('FeaturesService', () => {
|
||||
internalEventBus = {} as jest.Mocked<InternalEventBusInterface>
|
||||
internalEventBus.publish = jest.fn()
|
||||
internalEventBus.addEventHandler = jest.fn()
|
||||
|
||||
featureService = new FeaturesService(
|
||||
storageService,
|
||||
itemManager,
|
||||
mutator,
|
||||
subscriptions,
|
||||
apiService,
|
||||
webSocketsService,
|
||||
settingsService,
|
||||
userService,
|
||||
syncService,
|
||||
alertService,
|
||||
sessionManager,
|
||||
crypto,
|
||||
internalEventBus,
|
||||
)
|
||||
})
|
||||
|
||||
describe('experimental features', () => {
|
||||
it('enables/disables an experimental feature', async () => {
|
||||
storageService.getValue = jest.fn().mockReturnValue(GetFeatures())
|
||||
|
||||
const featuresService = createService()
|
||||
featuresService.getExperimentalFeatures = jest.fn().mockReturnValue([FeatureIdentifier.PlusEditor])
|
||||
featuresService.initializeFromDisk()
|
||||
featureService.getExperimentalFeatures = jest.fn().mockReturnValue([NativeFeatureIdentifier.TYPES.PlusEditor])
|
||||
featureService.initializeFromDisk()
|
||||
|
||||
featuresService.enableExperimentalFeature(FeatureIdentifier.PlusEditor)
|
||||
featureService.enableExperimentalFeature(NativeFeatureIdentifier.TYPES.PlusEditor)
|
||||
|
||||
expect(featuresService.isExperimentalFeatureEnabled(FeatureIdentifier.PlusEditor)).toEqual(true)
|
||||
expect(featureService.isExperimentalFeatureEnabled(NativeFeatureIdentifier.TYPES.PlusEditor)).toEqual(true)
|
||||
|
||||
featuresService.disableExperimentalFeature(FeatureIdentifier.PlusEditor)
|
||||
featureService.disableExperimentalFeature(NativeFeatureIdentifier.TYPES.PlusEditor)
|
||||
|
||||
expect(featuresService.isExperimentalFeatureEnabled(FeatureIdentifier.PlusEditor)).toEqual(false)
|
||||
expect(featureService.isExperimentalFeatureEnabled(NativeFeatureIdentifier.TYPES.PlusEditor)).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('hasFirstPartyOnlineSubscription', () => {
|
||||
it('should be true if signed into first party server and has online subscription', () => {
|
||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
|
||||
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true)
|
||||
|
||||
expect(featureService.hasFirstPartyOnlineSubscription()).toEqual(true)
|
||||
})
|
||||
|
||||
it('should not be true if not signed into first party server', () => {
|
||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false)
|
||||
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true)
|
||||
|
||||
expect(featureService.hasFirstPartyOnlineSubscription()).toEqual(false)
|
||||
})
|
||||
|
||||
it('should not be true if no online subscription', () => {
|
||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
|
||||
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(false)
|
||||
|
||||
expect(featureService.hasFirstPartyOnlineSubscription()).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('hasPaidAnyPartyOnlineOrOfflineSubscription', () => {
|
||||
it('should return true if onlineRolesIncludePaidSubscription', () => {
|
||||
featureService.onlineRolesIncludePaidSubscription = jest.fn().mockReturnValue(true)
|
||||
|
||||
expect(featureService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true)
|
||||
})
|
||||
|
||||
it('should return true if hasOfflineRepo', () => {
|
||||
featureService.hasOfflineRepo = jest.fn().mockReturnValue(true)
|
||||
|
||||
expect(featureService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true)
|
||||
})
|
||||
|
||||
it('should return true if hasFirstPartyOnlineSubscription', () => {
|
||||
featureService.hasFirstPartyOnlineSubscription = jest.fn().mockReturnValue(true)
|
||||
|
||||
expect(featureService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -148,40 +207,40 @@ describe('FeaturesService', () => {
|
||||
describe('updateRoles()', () => {
|
||||
it('setRoles should notify event if roles changed', async () => {
|
||||
storageService.getValue = jest.fn().mockReturnValue(roles)
|
||||
const featuresService = createService()
|
||||
featuresService.initializeFromDisk()
|
||||
|
||||
const mock = (featuresService['notifyEvent'] = jest.fn())
|
||||
featureService.initializeFromDisk()
|
||||
|
||||
const mock = (featureService['notifyEvent'] = jest.fn())
|
||||
|
||||
const newRoles = [...roles, RoleName.NAMES.PlusUser]
|
||||
featuresService.setOnlineRoles(newRoles)
|
||||
featureService.setOnlineRoles(newRoles)
|
||||
|
||||
expect(mock.mock.calls[0][0]).toEqual(FeaturesEvent.UserRolesChanged)
|
||||
})
|
||||
|
||||
it('should notify of subscription purchase', async () => {
|
||||
storageService.getValue = jest.fn().mockReturnValue(roles)
|
||||
const featuresService = createService()
|
||||
featuresService.initializeFromDisk()
|
||||
|
||||
const spy = jest.spyOn(featuresService, 'notifyEvent' as never)
|
||||
featureService.initializeFromDisk()
|
||||
|
||||
const spy = jest.spyOn(featureService, 'notifyEvent' as never)
|
||||
|
||||
const newRoles = [...roles, RoleName.NAMES.ProUser]
|
||||
await featuresService.updateOnlineRolesWithNewValues(newRoles)
|
||||
await featureService.updateOnlineRolesWithNewValues(newRoles)
|
||||
|
||||
expect(spy.mock.calls[1][0]).toEqual(FeaturesEvent.DidPurchaseSubscription)
|
||||
})
|
||||
|
||||
it('should not notify of subscription purchase on initial roles load after sign in', async () => {
|
||||
storageService.getValue = jest.fn().mockReturnValue(roles)
|
||||
const featuresService = createService()
|
||||
featuresService.initializeFromDisk()
|
||||
featuresService['onlineRoles'] = []
|
||||
|
||||
const spy = jest.spyOn(featuresService, 'notifyEvent' as never)
|
||||
featureService.initializeFromDisk()
|
||||
featureService['onlineRoles'] = []
|
||||
|
||||
const spy = jest.spyOn(featureService, 'notifyEvent' as never)
|
||||
|
||||
const newRoles = [...roles, RoleName.NAMES.ProUser]
|
||||
await featuresService.updateOnlineRolesWithNewValues(newRoles)
|
||||
await featureService.updateOnlineRolesWithNewValues(newRoles)
|
||||
|
||||
const triggeredEvents = spy.mock.calls.map((call) => call[0])
|
||||
expect(triggeredEvents).not.toContain(FeaturesEvent.DidPurchaseSubscription)
|
||||
@@ -189,11 +248,11 @@ describe('FeaturesService', () => {
|
||||
|
||||
it('saves new roles to storage if a role has been added', async () => {
|
||||
storageService.getValue = jest.fn().mockReturnValue(roles)
|
||||
const featuresService = createService()
|
||||
featuresService.initializeFromDisk()
|
||||
|
||||
featureService.initializeFromDisk()
|
||||
|
||||
const newRoles = [...roles, RoleName.NAMES.ProUser]
|
||||
await featuresService.updateOnlineRolesWithNewValues(newRoles)
|
||||
await featureService.updateOnlineRolesWithNewValues(newRoles)
|
||||
expect(storageService.setValue).toHaveBeenCalledWith(StorageKey.UserRoles, newRoles)
|
||||
})
|
||||
|
||||
@@ -201,134 +260,162 @@ describe('FeaturesService', () => {
|
||||
const newRoles = [RoleName.NAMES.CoreUser]
|
||||
|
||||
storageService.getValue = jest.fn().mockReturnValue(roles)
|
||||
const featuresService = createService()
|
||||
featuresService.initializeFromDisk()
|
||||
await featuresService.updateOnlineRolesWithNewValues(newRoles)
|
||||
|
||||
featureService.initializeFromDisk()
|
||||
await featureService.updateOnlineRolesWithNewValues(newRoles)
|
||||
|
||||
expect(storageService.setValue).toHaveBeenCalledWith(StorageKey.UserRoles, newRoles)
|
||||
})
|
||||
|
||||
it('role-based feature status', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
|
||||
|
||||
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
||||
await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
||||
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true)
|
||||
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled)
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.SuperEditor)).toBe(FeatureStatus.Entitled)
|
||||
expect(
|
||||
featureService.getFeatureStatus(
|
||||
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.MidnightTheme).getValue(),
|
||||
),
|
||||
).toBe(FeatureStatus.Entitled)
|
||||
expect(
|
||||
featureService.getFeatureStatus(
|
||||
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SuperEditor).getValue(),
|
||||
),
|
||||
).toBe(FeatureStatus.Entitled)
|
||||
})
|
||||
|
||||
it('feature status with no paid role', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
|
||||
|
||||
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
|
||||
await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
|
||||
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(false)
|
||||
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.NoUserSubscription)
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.PlusEditor)).toBe(FeatureStatus.NoUserSubscription)
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.SheetsEditor)).toBe(FeatureStatus.NoUserSubscription)
|
||||
expect(
|
||||
featureService.getFeatureStatus(
|
||||
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.MidnightTheme).getValue(),
|
||||
),
|
||||
).toBe(FeatureStatus.NoUserSubscription)
|
||||
expect(
|
||||
featureService.getFeatureStatus(
|
||||
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.PlusEditor).getValue(),
|
||||
),
|
||||
).toBe(FeatureStatus.NoUserSubscription)
|
||||
expect(
|
||||
featureService.getFeatureStatus(
|
||||
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SheetsEditor).getValue(),
|
||||
),
|
||||
).toBe(FeatureStatus.NoUserSubscription)
|
||||
})
|
||||
|
||||
it('role-based features while not signed into first party server', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false)
|
||||
|
||||
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.ProUser])
|
||||
await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.ProUser])
|
||||
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.SuperEditor)).toBe(FeatureStatus.NoUserSubscription)
|
||||
expect(
|
||||
featureService.getFeatureStatus(
|
||||
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SuperEditor).getValue(),
|
||||
),
|
||||
).toBe(FeatureStatus.NoUserSubscription)
|
||||
})
|
||||
|
||||
it('third party feature status', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
itemManager.getDisplayableComponents = jest
|
||||
.fn()
|
||||
.mockReturnValue([{ identifier: 'third-party-theme' }, { identifier: 'third-party-editor', isExpired: true }])
|
||||
.mockReturnValue([
|
||||
{ uuid: '00000000-0000-0000-0000-000000000001' },
|
||||
{ uuid: '00000000-0000-0000-0000-000000000002', isExpired: true },
|
||||
])
|
||||
|
||||
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
|
||||
await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
|
||||
|
||||
expect(featuresService.getFeatureStatus('third-party-theme' as FeatureIdentifier)).toBe(FeatureStatus.Entitled)
|
||||
expect(featuresService.getFeatureStatus('third-party-editor' as FeatureIdentifier)).toBe(
|
||||
expect(featureService.getFeatureStatus(Uuid.create('00000000-0000-0000-0000-000000000001').getValue())).toBe(
|
||||
FeatureStatus.Entitled,
|
||||
)
|
||||
expect(featureService.getFeatureStatus(Uuid.create('00000000-0000-0000-0000-000000000002').getValue())).toBe(
|
||||
FeatureStatus.InCurrentPlanButExpired,
|
||||
)
|
||||
expect(featuresService.getFeatureStatus('missing-feature-identifier' as FeatureIdentifier)).toBe(
|
||||
expect(featureService.getFeatureStatus(Uuid.create('00000000-0000-0000-0000-000000000003').getValue())).toBe(
|
||||
FeatureStatus.NoUserSubscription,
|
||||
)
|
||||
})
|
||||
|
||||
it('feature status should be not entitled if no account or offline repo', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
|
||||
await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
|
||||
|
||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false)
|
||||
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.NoUserSubscription)
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.TokenVaultEditor)).toBe(
|
||||
FeatureStatus.NoUserSubscription,
|
||||
)
|
||||
expect(
|
||||
featureService.getFeatureStatus(
|
||||
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.MidnightTheme).getValue(),
|
||||
),
|
||||
).toBe(FeatureStatus.NoUserSubscription)
|
||||
expect(
|
||||
featureService.getFeatureStatus(
|
||||
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.TokenVaultEditor).getValue(),
|
||||
),
|
||||
).toBe(FeatureStatus.NoUserSubscription)
|
||||
})
|
||||
|
||||
it('feature status for offline subscription', async () => {
|
||||
const featuresService = createService()
|
||||
featureService.hasFirstPartyOfflineSubscription = jest.fn().mockReturnValue(true)
|
||||
featureService.setOfflineRoles([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
||||
|
||||
featuresService.hasFirstPartyOfflineSubscription = jest.fn().mockReturnValue(true)
|
||||
featuresService.setOfflineRoles([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
||||
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled)
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.TokenVaultEditor)).toBe(FeatureStatus.Entitled)
|
||||
expect(
|
||||
featureService.getFeatureStatus(
|
||||
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.MidnightTheme).getValue(),
|
||||
),
|
||||
).toBe(FeatureStatus.Entitled)
|
||||
expect(
|
||||
featureService.getFeatureStatus(
|
||||
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.TokenVaultEditor).getValue(),
|
||||
),
|
||||
).toBe(FeatureStatus.Entitled)
|
||||
})
|
||||
|
||||
it('feature status for deprecated feature and no subscription', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(false)
|
||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
|
||||
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.DeprecatedFileSafe as FeatureIdentifier)).toBe(
|
||||
FeatureStatus.NoUserSubscription,
|
||||
)
|
||||
expect(
|
||||
featureService.getFeatureStatus(
|
||||
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.DeprecatedFileSafe).getValue(),
|
||||
),
|
||||
).toBe(FeatureStatus.NoUserSubscription)
|
||||
})
|
||||
|
||||
it('feature status for deprecated feature with subscription', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true)
|
||||
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
||||
await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
||||
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.DeprecatedFileSafe as FeatureIdentifier)).toBe(
|
||||
FeatureStatus.Entitled,
|
||||
)
|
||||
expect(
|
||||
featureService.getFeatureStatus(
|
||||
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.DeprecatedFileSafe).getValue(),
|
||||
),
|
||||
).toBe(FeatureStatus.Entitled)
|
||||
})
|
||||
|
||||
it('has paid subscription', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
|
||||
await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
|
||||
|
||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
|
||||
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true)
|
||||
|
||||
expect(featuresService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toBeFalsy
|
||||
expect(featureService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toBeFalsy
|
||||
|
||||
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
||||
await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
||||
|
||||
expect(featuresService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true)
|
||||
expect(featureService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true)
|
||||
})
|
||||
|
||||
it('has paid subscription should be true if offline repo and signed into third party server', async () => {
|
||||
const featuresService = createService()
|
||||
await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
|
||||
|
||||
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
|
||||
|
||||
featuresService.hasOfflineRepo = jest.fn().mockReturnValue(true)
|
||||
featureService.hasOfflineRepo = jest.fn().mockReturnValue(true)
|
||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false)
|
||||
|
||||
expect(featuresService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true)
|
||||
expect(featureService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -343,8 +430,7 @@ describe('FeaturesService', () => {
|
||||
},
|
||||
} as never)
|
||||
|
||||
const featuresService = createService()
|
||||
await featuresService.migrateFeatureRepoToUserSetting([extensionRepoItem])
|
||||
await featureService.migrateFeatureRepoToUserSetting([extensionRepoItem])
|
||||
expect(settingsService.updateSetting).toHaveBeenCalledWith(
|
||||
SettingName.create(SettingName.NAMES.ExtensionKey).getValue(),
|
||||
extensionKey,
|
||||
@@ -369,8 +455,7 @@ describe('FeaturesService', () => {
|
||||
const installUrl = 'http://example.com'
|
||||
crypto.base64Decode = jest.fn().mockReturnValue(installUrl)
|
||||
|
||||
const featuresService = createService()
|
||||
const result = await featuresService.downloadRemoteThirdPartyFeature(installUrl)
|
||||
const result = await featureService.downloadRemoteThirdPartyFeature(installUrl)
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -389,17 +474,14 @@ describe('FeaturesService', () => {
|
||||
const installUrl = 'http://example.com'
|
||||
crypto.base64Decode = jest.fn().mockReturnValue(installUrl)
|
||||
|
||||
const featuresService = createService()
|
||||
const result = await featuresService.downloadRemoteThirdPartyFeature(installUrl)
|
||||
const result = await featureService.downloadRemoteThirdPartyFeature(installUrl)
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('sortRolesByHierarchy', () => {
|
||||
it('should sort given roles according to role hierarchy', () => {
|
||||
const featuresService = createService()
|
||||
|
||||
const sortedRoles = featuresService.rolesBySorting([
|
||||
const sortedRoles = featureService.rolesBySorting([
|
||||
RoleName.NAMES.ProUser,
|
||||
RoleName.NAMES.CoreUser,
|
||||
RoleName.NAMES.PlusUser,
|
||||
@@ -411,50 +493,42 @@ describe('FeaturesService', () => {
|
||||
|
||||
describe('hasMinimumRole', () => {
|
||||
it('should be false if core user checks for plus role', async () => {
|
||||
const featuresService = createService()
|
||||
await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
|
||||
|
||||
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.CoreUser])
|
||||
|
||||
const hasPlusUserRole = featuresService.hasMinimumRole(RoleName.NAMES.PlusUser)
|
||||
const hasPlusUserRole = featureService.hasMinimumRole(RoleName.NAMES.PlusUser)
|
||||
|
||||
expect(hasPlusUserRole).toBe(false)
|
||||
})
|
||||
|
||||
it('should be false if plus user checks for pro role', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
|
||||
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true)
|
||||
|
||||
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.PlusUser, RoleName.NAMES.CoreUser])
|
||||
await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.PlusUser, RoleName.NAMES.CoreUser])
|
||||
|
||||
const hasProUserRole = featuresService.hasMinimumRole(RoleName.NAMES.ProUser)
|
||||
const hasProUserRole = featureService.hasMinimumRole(RoleName.NAMES.ProUser)
|
||||
|
||||
expect(hasProUserRole).toBe(false)
|
||||
})
|
||||
|
||||
it('should be true if pro user checks for core user', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
|
||||
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true)
|
||||
|
||||
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser])
|
||||
await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser])
|
||||
|
||||
const hasCoreUserRole = featuresService.hasMinimumRole(RoleName.NAMES.CoreUser)
|
||||
const hasCoreUserRole = featureService.hasMinimumRole(RoleName.NAMES.CoreUser)
|
||||
|
||||
expect(hasCoreUserRole).toBe(true)
|
||||
})
|
||||
|
||||
it('should be true if pro user checks for pro user', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
|
||||
subscriptions.hasOnlineSubscription = jest.fn().mockReturnValue(true)
|
||||
|
||||
await featuresService.updateOnlineRolesWithNewValues([RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser])
|
||||
await featureService.updateOnlineRolesWithNewValues([RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser])
|
||||
|
||||
const hasProUserRole = featuresService.hasMinimumRole(RoleName.NAMES.ProUser)
|
||||
const hasProUserRole = featureService.hasMinimumRole(RoleName.NAMES.ProUser)
|
||||
|
||||
expect(hasProUserRole).toBe(true)
|
||||
})
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { MigrateFeatureRepoToUserSettingUseCase } from './UseCase/MigrateFeatureRepoToUserSetting'
|
||||
import { arraysEqual, removeFromArray, lastElement } from '@standardnotes/utils'
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
import { RoleName, ContentType } from '@standardnotes/domain-core'
|
||||
import { RoleName, ContentType, Uuid } from '@standardnotes/domain-core'
|
||||
import { PROD_OFFLINE_FEATURES_URL } from '../../Hosts'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { WebSocketsService } from '../Api/WebsocketsService'
|
||||
import { WebSocketsServiceEvent } from '../Api/WebSocketsServiceEvent'
|
||||
import { TRUSTED_CUSTOM_EXTENSIONS_HOSTS, TRUSTED_FEATURE_HOSTS } from '@Lib/Hosts'
|
||||
import { UserRolesChangedEvent } from '@standardnotes/domain-events'
|
||||
import { ExperimentalFeatures, FindNativeFeature, FeatureIdentifier } from '@standardnotes/features'
|
||||
import { ExperimentalFeatures, FindNativeFeature, NativeFeatureIdentifier } from '@standardnotes/features'
|
||||
import {
|
||||
SNFeatureRepo,
|
||||
FeatureRepoContent,
|
||||
@@ -64,7 +64,7 @@ export class FeaturesService
|
||||
{
|
||||
private onlineRoles: string[] = []
|
||||
private offlineRoles: string[] = []
|
||||
private enabledExperimentalFeatures: FeatureIdentifier[] = []
|
||||
private enabledExperimentalFeatures: string[] = []
|
||||
|
||||
private getFeatureStatusUseCase = new GetFeatureStatusUseCase(this.items)
|
||||
|
||||
@@ -136,40 +136,47 @@ export class FeaturesService
|
||||
)
|
||||
}
|
||||
|
||||
public initializeFromDisk(): void {
|
||||
initializeFromDisk(): void {
|
||||
this.onlineRoles = this.storage.getValue<string[]>(StorageKey.UserRoles, undefined, [])
|
||||
|
||||
this.offlineRoles = this.storage.getValue<string[]>(StorageKey.OfflineUserRoles, undefined, [])
|
||||
|
||||
this.enabledExperimentalFeatures = this.storage.getValue(StorageKey.ExperimentalFeatures, undefined, [])
|
||||
}
|
||||
|
||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||
if (event.type === ApiServiceEvent.MetaReceived) {
|
||||
if (!this.sync) {
|
||||
this.log('Handling events interrupted. Sync service is not yet initialized.', event)
|
||||
return
|
||||
switch (event.type) {
|
||||
case ApiServiceEvent.MetaReceived: {
|
||||
if (!this.sync) {
|
||||
this.log('Handling events interrupted. Sync service is not yet initialized.', event)
|
||||
return
|
||||
}
|
||||
|
||||
const { userRoles } = event.payload as MetaReceivedData
|
||||
void this.updateOnlineRolesWithNewValues(userRoles.map((role) => role.name))
|
||||
break
|
||||
}
|
||||
|
||||
const { userRoles } = event.payload as MetaReceivedData
|
||||
void this.updateOnlineRolesWithNewValues(userRoles.map((role) => role.name))
|
||||
}
|
||||
|
||||
if (event.type === ApplicationEvent.ApplicationStageChanged) {
|
||||
const stage = (event.payload as ApplicationStageChangedEventPayload).stage
|
||||
if (stage === ApplicationStage.FullSyncCompleted_13) {
|
||||
if (!this.hasFirstPartyOnlineSubscription()) {
|
||||
const offlineRepo = this.getOfflineRepo()
|
||||
|
||||
if (offlineRepo) {
|
||||
void this.downloadOfflineRoles(offlineRepo)
|
||||
case ApplicationEvent.ApplicationStageChanged: {
|
||||
const stage = (event.payload as ApplicationStageChangedEventPayload).stage
|
||||
switch (stage) {
|
||||
case ApplicationStage.StorageDecrypted_09: {
|
||||
this.initializeFromDisk()
|
||||
break
|
||||
}
|
||||
case ApplicationStage.FullSyncCompleted_13: {
|
||||
if (!this.hasFirstPartyOnlineSubscription()) {
|
||||
const offlineRepo = this.getOfflineRepo()
|
||||
if (offlineRepo) {
|
||||
void this.downloadOfflineRoles(offlineRepo)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enableExperimentalFeature(identifier: FeatureIdentifier): void {
|
||||
public enableExperimentalFeature(identifier: string): void {
|
||||
this.enabledExperimentalFeatures.push(identifier)
|
||||
|
||||
void this.storage.setValue(StorageKey.ExperimentalFeatures, this.enabledExperimentalFeatures)
|
||||
@@ -177,7 +184,7 @@ export class FeaturesService
|
||||
void this.notifyEvent(FeaturesEvent.FeaturesAvailabilityChanged)
|
||||
}
|
||||
|
||||
public disableExperimentalFeature(identifier: FeatureIdentifier): void {
|
||||
public disableExperimentalFeature(identifier: string): void {
|
||||
removeFromArray(this.enabledExperimentalFeatures, identifier)
|
||||
|
||||
void this.storage.setValue(StorageKey.ExperimentalFeatures, this.enabledExperimentalFeatures)
|
||||
@@ -195,7 +202,7 @@ export class FeaturesService
|
||||
void this.notifyEvent(FeaturesEvent.FeaturesAvailabilityChanged)
|
||||
}
|
||||
|
||||
public toggleExperimentalFeature(identifier: FeatureIdentifier): void {
|
||||
public toggleExperimentalFeature(identifier: string): void {
|
||||
if (this.isExperimentalFeatureEnabled(identifier)) {
|
||||
this.disableExperimentalFeature(identifier)
|
||||
} else {
|
||||
@@ -203,19 +210,19 @@ export class FeaturesService
|
||||
}
|
||||
}
|
||||
|
||||
public getExperimentalFeatures(): FeatureIdentifier[] {
|
||||
public getExperimentalFeatures(): string[] {
|
||||
return ExperimentalFeatures
|
||||
}
|
||||
|
||||
public isExperimentalFeature(featureId: FeatureIdentifier): boolean {
|
||||
public isExperimentalFeature(featureId: string): boolean {
|
||||
return this.getExperimentalFeatures().includes(featureId)
|
||||
}
|
||||
|
||||
public getEnabledExperimentalFeatures(): FeatureIdentifier[] {
|
||||
public getEnabledExperimentalFeatures(): string[] {
|
||||
return this.enabledExperimentalFeatures
|
||||
}
|
||||
|
||||
public isExperimentalFeatureEnabled(featureId: FeatureIdentifier): boolean {
|
||||
public isExperimentalFeatureEnabled(featureId: string): boolean {
|
||||
return this.enabledExperimentalFeatures.includes(featureId)
|
||||
}
|
||||
|
||||
@@ -302,10 +309,10 @@ export class FeaturesService
|
||||
}
|
||||
|
||||
hasPaidAnyPartyOnlineOrOfflineSubscription(): boolean {
|
||||
return this.onlineRolesIncludePaidSubscription() || this.hasOfflineRepo()
|
||||
return this.onlineRolesIncludePaidSubscription() || this.hasOfflineRepo() || this.hasFirstPartyOnlineSubscription()
|
||||
}
|
||||
|
||||
private hasFirstPartyOnlineSubscription(): boolean {
|
||||
hasFirstPartyOnlineSubscription(): boolean {
|
||||
return this.sessions.isSignedIntoFirstPartyServer() && this.subscriptions.hasOnlineSubscription()
|
||||
}
|
||||
|
||||
@@ -364,12 +371,13 @@ export class FeaturesService
|
||||
}
|
||||
|
||||
public isThirdPartyFeature(identifier: string): boolean {
|
||||
const isNativeFeature = !!FindNativeFeature(identifier as FeatureIdentifier)
|
||||
const isNativeFeature = !!FindNativeFeature(identifier)
|
||||
return !isNativeFeature
|
||||
}
|
||||
|
||||
onlineRolesIncludePaidSubscription(): boolean {
|
||||
const unpaidRoles = [RoleName.NAMES.CoreUser]
|
||||
|
||||
return this.onlineRoles.some((role) => !unpaidRoles.includes(role))
|
||||
}
|
||||
|
||||
@@ -392,7 +400,7 @@ export class FeaturesService
|
||||
}
|
||||
|
||||
public getFeatureStatus(
|
||||
featureId: FeatureIdentifier,
|
||||
featureId: NativeFeatureIdentifier | Uuid,
|
||||
options: { inContextOfItem?: DecryptedItemInterface } = {},
|
||||
): FeatureStatus {
|
||||
return this.getFeatureStatusUseCase.execute({
|
||||
|
||||
@@ -1,28 +1,23 @@
|
||||
import { FeatureIdentifier } from '@standardnotes/features'
|
||||
import { NativeFeatureIdentifier } from '@standardnotes/features'
|
||||
import { FeatureStatus, ItemManagerInterface } from '@standardnotes/services'
|
||||
import { GetFeatureStatusUseCase } from './GetFeatureStatus'
|
||||
import { ComponentInterface, DecryptedItemInterface } from '@standardnotes/models'
|
||||
|
||||
jest.mock('@standardnotes/features', () => ({
|
||||
FeatureIdentifier: {
|
||||
DarkTheme: 'darkTheme',
|
||||
},
|
||||
FindNativeFeature: jest.fn(),
|
||||
}))
|
||||
|
||||
import { FindNativeFeature } from '@standardnotes/features'
|
||||
import { Subscription } from '@standardnotes/responses'
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
describe('GetFeatureStatusUseCase', () => {
|
||||
let items: jest.Mocked<ItemManagerInterface>
|
||||
let usecase: GetFeatureStatusUseCase
|
||||
let findNativeFeature: jest.Mock<any, any>
|
||||
|
||||
beforeEach(() => {
|
||||
items = {
|
||||
getDisplayableComponents: jest.fn(),
|
||||
} as unknown as jest.Mocked<ItemManagerInterface>
|
||||
usecase = new GetFeatureStatusUseCase(items)
|
||||
;(FindNativeFeature as jest.Mock).mockReturnValue(undefined)
|
||||
findNativeFeature = jest.fn()
|
||||
usecase.findNativeFeature = findNativeFeature
|
||||
findNativeFeature.mockReturnValue(undefined)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -33,7 +28,7 @@ describe('GetFeatureStatusUseCase', () => {
|
||||
it('should return entitled for free features', () => {
|
||||
expect(
|
||||
usecase.execute({
|
||||
featureId: FeatureIdentifier.DarkTheme,
|
||||
featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.DarkTheme).getValue(),
|
||||
hasPaidAnyPartyOnlineOrOfflineSubscription: false,
|
||||
firstPartyOnlineSubscription: undefined,
|
||||
firstPartyRoles: undefined,
|
||||
@@ -44,11 +39,11 @@ describe('GetFeatureStatusUseCase', () => {
|
||||
|
||||
describe('deprecated features', () => {
|
||||
it('should return entitled for deprecated paid features if any subscription is active', () => {
|
||||
;(FindNativeFeature as jest.Mock).mockReturnValue({ deprecated: true })
|
||||
findNativeFeature.mockReturnValue({ deprecated: true })
|
||||
|
||||
expect(
|
||||
usecase.execute({
|
||||
featureId: 'deprecatedFeature',
|
||||
featureId: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
hasPaidAnyPartyOnlineOrOfflineSubscription: true,
|
||||
firstPartyOnlineSubscription: undefined,
|
||||
firstPartyRoles: undefined,
|
||||
@@ -57,11 +52,11 @@ describe('GetFeatureStatusUseCase', () => {
|
||||
})
|
||||
|
||||
it('should return NoUserSubscription for deprecated paid features if no subscription is active', () => {
|
||||
;(FindNativeFeature as jest.Mock).mockReturnValue({ deprecated: true })
|
||||
findNativeFeature.mockReturnValue({ deprecated: true })
|
||||
|
||||
expect(
|
||||
usecase.execute({
|
||||
featureId: 'deprecatedFeature',
|
||||
featureId: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
hasPaidAnyPartyOnlineOrOfflineSubscription: false,
|
||||
firstPartyOnlineSubscription: undefined,
|
||||
firstPartyRoles: undefined,
|
||||
@@ -72,11 +67,11 @@ describe('GetFeatureStatusUseCase', () => {
|
||||
|
||||
describe('native features', () => {
|
||||
it('should return Entitled if the context item belongs to a shared vault and user does not have subscription', () => {
|
||||
;(FindNativeFeature as jest.Mock).mockReturnValue({ deprecated: false })
|
||||
findNativeFeature.mockReturnValue({ deprecated: false })
|
||||
|
||||
expect(
|
||||
usecase.execute({
|
||||
featureId: 'nativeFeature',
|
||||
featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.AutobiographyTheme).getValue(),
|
||||
firstPartyOnlineSubscription: undefined,
|
||||
firstPartyRoles: undefined,
|
||||
hasPaidAnyPartyOnlineOrOfflineSubscription: false,
|
||||
@@ -86,11 +81,11 @@ describe('GetFeatureStatusUseCase', () => {
|
||||
})
|
||||
|
||||
it('should return NoUserSubscription if the context item does not belong to a shared vault and user does not have subscription', () => {
|
||||
;(FindNativeFeature as jest.Mock).mockReturnValue({ deprecated: false })
|
||||
findNativeFeature.mockReturnValue({ deprecated: false })
|
||||
|
||||
expect(
|
||||
usecase.execute({
|
||||
featureId: 'nativeFeature',
|
||||
featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.AutobiographyTheme).getValue(),
|
||||
firstPartyOnlineSubscription: undefined,
|
||||
firstPartyRoles: undefined,
|
||||
hasPaidAnyPartyOnlineOrOfflineSubscription: false,
|
||||
@@ -100,11 +95,11 @@ describe('GetFeatureStatusUseCase', () => {
|
||||
})
|
||||
|
||||
it('should return NoUserSubscription for native features without subscription and roles', () => {
|
||||
;(FindNativeFeature as jest.Mock).mockReturnValue({ deprecated: false })
|
||||
findNativeFeature.mockReturnValue({ deprecated: false })
|
||||
|
||||
expect(
|
||||
usecase.execute({
|
||||
featureId: 'nativeFeature',
|
||||
featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.AutobiographyTheme).getValue(),
|
||||
firstPartyOnlineSubscription: undefined,
|
||||
firstPartyRoles: undefined,
|
||||
hasPaidAnyPartyOnlineOrOfflineSubscription: false,
|
||||
@@ -113,14 +108,14 @@ describe('GetFeatureStatusUseCase', () => {
|
||||
})
|
||||
|
||||
it('should return NotInCurrentPlan for native features with roles not in available roles', () => {
|
||||
;(FindNativeFeature as jest.Mock).mockReturnValue({
|
||||
findNativeFeature.mockReturnValue({
|
||||
deprecated: false,
|
||||
availableInRoles: ['notInRole'],
|
||||
})
|
||||
|
||||
expect(
|
||||
usecase.execute({
|
||||
featureId: 'nativeFeature',
|
||||
featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.AutobiographyTheme).getValue(),
|
||||
firstPartyOnlineSubscription: undefined,
|
||||
firstPartyRoles: { online: ['inRole'] },
|
||||
hasPaidAnyPartyOnlineOrOfflineSubscription: false,
|
||||
@@ -129,14 +124,14 @@ describe('GetFeatureStatusUseCase', () => {
|
||||
})
|
||||
|
||||
it('should return Entitled for native features with roles in available roles and active subscription', () => {
|
||||
;(FindNativeFeature as jest.Mock).mockReturnValue({
|
||||
findNativeFeature.mockReturnValue({
|
||||
deprecated: false,
|
||||
availableInRoles: ['inRole'],
|
||||
})
|
||||
|
||||
expect(
|
||||
usecase.execute({
|
||||
featureId: 'nativeFeature',
|
||||
featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.AutobiographyTheme).getValue(),
|
||||
firstPartyOnlineSubscription: {
|
||||
endsAt: new Date(Date.now() + 10000).getTime(),
|
||||
} as jest.Mocked<Subscription>,
|
||||
@@ -147,14 +142,14 @@ describe('GetFeatureStatusUseCase', () => {
|
||||
})
|
||||
|
||||
it('should return InCurrentPlanButExpired for native features with roles in available roles and expired subscription', () => {
|
||||
;(FindNativeFeature as jest.Mock).mockReturnValue({
|
||||
findNativeFeature.mockReturnValue({
|
||||
deprecated: false,
|
||||
availableInRoles: ['inRole'],
|
||||
})
|
||||
|
||||
expect(
|
||||
usecase.execute({
|
||||
featureId: 'nativeFeature',
|
||||
featureId: NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.AutobiographyTheme).getValue(),
|
||||
firstPartyOnlineSubscription: {
|
||||
endsAt: new Date(Date.now() - 10000).getTime(),
|
||||
} as jest.Mocked<Subscription>,
|
||||
@@ -168,7 +163,7 @@ describe('GetFeatureStatusUseCase', () => {
|
||||
describe('third party features', () => {
|
||||
it('should return Entitled for third-party features', () => {
|
||||
const mockComponent = {
|
||||
identifier: 'thirdPartyFeature',
|
||||
uuid: '00000000-0000-0000-0000-000000000000',
|
||||
isExpired: false,
|
||||
} as unknown as jest.Mocked<ComponentInterface>
|
||||
|
||||
@@ -176,7 +171,7 @@ describe('GetFeatureStatusUseCase', () => {
|
||||
|
||||
expect(
|
||||
usecase.execute({
|
||||
featureId: 'thirdPartyFeature',
|
||||
featureId: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
hasPaidAnyPartyOnlineOrOfflineSubscription: false,
|
||||
firstPartyOnlineSubscription: undefined,
|
||||
firstPartyRoles: undefined,
|
||||
@@ -189,7 +184,7 @@ describe('GetFeatureStatusUseCase', () => {
|
||||
|
||||
expect(
|
||||
usecase.execute({
|
||||
featureId: 'nonExistingThirdPartyFeature',
|
||||
featureId: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
hasPaidAnyPartyOnlineOrOfflineSubscription: false,
|
||||
firstPartyOnlineSubscription: undefined,
|
||||
firstPartyRoles: undefined,
|
||||
@@ -199,7 +194,7 @@ describe('GetFeatureStatusUseCase', () => {
|
||||
|
||||
it('should return InCurrentPlanButExpired for expired third-party features', () => {
|
||||
const mockComponent = {
|
||||
identifier: 'thirdPartyFeature',
|
||||
uuid: '00000000-0000-0000-0000-000000000000',
|
||||
isExpired: true,
|
||||
} as unknown as jest.Mocked<ComponentInterface>
|
||||
|
||||
@@ -207,7 +202,7 @@ describe('GetFeatureStatusUseCase', () => {
|
||||
|
||||
expect(
|
||||
usecase.execute({
|
||||
featureId: 'thirdPartyFeature',
|
||||
featureId: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
|
||||
hasPaidAnyPartyOnlineOrOfflineSubscription: false,
|
||||
firstPartyOnlineSubscription: undefined,
|
||||
firstPartyRoles: undefined,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,13 +286,11 @@ export class PayloadManager extends AbstractService implements PayloadManagerInt
|
||||
/**
|
||||
* Imports an array of payloads from an external source (such as a backup file)
|
||||
* and marks the items as dirty.
|
||||
* @returns Resulting items
|
||||
*/
|
||||
public async importPayloads(payloads: DecryptedPayloadInterface[], historyMap: HistoryMap): Promise<string[]> {
|
||||
public async importPayloads(payloads: FullyFormedPayloadInterface[], historyMap: HistoryMap): Promise<string[]> {
|
||||
const sourcedPayloads = payloads.map((p) => p.copy(undefined, PayloadSource.FileImport))
|
||||
|
||||
const delta = new DeltaFileImport(this.getMasterCollection(), sourcedPayloads, historyMap)
|
||||
|
||||
const emit = delta.result()
|
||||
|
||||
await this.emitDeltaEmit(emit)
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
private async initializeFromDisk(): Promise<void> {
|
||||
this.memoizeUser(this.storage.getValue(StorageKey.User))
|
||||
|
||||
if (!this.user) {
|
||||
|
||||
@@ -6,6 +6,7 @@ export const VaultTests = {
|
||||
'vaults/pkc.test.js',
|
||||
'vaults/contacts.test.js',
|
||||
'vaults/crypto.test.js',
|
||||
'vaults/importing.test.js',
|
||||
'vaults/asymmetric-messages.test.js',
|
||||
'vaults/keypair-change.test.js',
|
||||
'vaults/signatures.test.js',
|
||||
@@ -16,7 +17,7 @@ export const VaultTests = {
|
||||
'vaults/conflicts.test.js',
|
||||
'vaults/deletion.test.js',
|
||||
'vaults/permissions.test.js',
|
||||
'vaults/key_rotation.test.js',
|
||||
'vaults/key-rotation.test.js',
|
||||
'vaults/files.test.js',
|
||||
],
|
||||
}
|
||||
|
||||
@@ -755,7 +755,7 @@ describe('keys', function () {
|
||||
currentServerPassword: currentRootKey.serverPassword,
|
||||
newRootKey,
|
||||
})
|
||||
await this.application.encryption.reencryptApplicableItemsAfterUserRootKeyChange()
|
||||
await this.application.dependencies.get(TYPES.ReencryptTypeAItems).execute()
|
||||
/** Note: this may result in a deadlock if features_service syncs and results in an error */
|
||||
await this.application.sync.sync({ awaitAll: true })
|
||||
|
||||
|
||||
@@ -369,6 +369,17 @@ export class AppContext {
|
||||
})
|
||||
}
|
||||
|
||||
spyOnFunctionResult(object, functionName) {
|
||||
return new Promise((resolve) => {
|
||||
sinon.stub(object, functionName).callsFake(async (params) => {
|
||||
object[functionName].restore()
|
||||
const result = await object[functionName](params)
|
||||
resolve(result)
|
||||
return result
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
resolveWhenAsymmetricMessageProcessingCompletes() {
|
||||
return this.resolveWhenAsyncFunctionCompletes(this.asymmetric, 'handleRemoteReceivedAsymmetricMessages')
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user