Compare commits

..
144 changed files with 1637 additions and 1013 deletions
+8
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api",
"version": "1.26.31",
"version": "1.26.33",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+8
View File
@@ -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 -1
View File
@@ -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",
+8
View File
@@ -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 -1
View File
@@ -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,
+8
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/encryption",
"version": "1.21.54",
"version": "1.21.56",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -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
+6
View File
@@ -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 -1
View File
@@ -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],
},
+8 -8
View File
@@ -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 -2
View File
@@ -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'
+8
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/filepicker",
"version": "1.28.63",
"version": "1.28.65",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+8
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files",
"version": "1.16.9",
"version": "1.16.11",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+8
View File
@@ -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 -1
View File
@@ -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",
+10
View File
@@ -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 -1
View File
@@ -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>>
+8
View File
@@ -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 -1
View File
@@ -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",
+4
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/responses",
"version": "1.13.31",
"version": "1.13.32",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+10
View File
@@ -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 -1
View File
@@ -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,
+1
View File
@@ -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'
+10
View File
@@ -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
+2 -4
View File
@@ -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,
])
/**
@@ -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)
}
@@ -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}`,
@@ -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',
],
}
+1 -1
View File
@@ -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 })
+11
View File
@@ -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