refactor: offline roles (#2169)
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
export enum FeatureIdentifier {
|
export enum FeatureIdentifier {
|
||||||
AccountSwitcher = 'com.standardnotes.account-switcher',
|
|
||||||
CloudLink = 'org.standardnotes.cloudlink',
|
CloudLink = 'org.standardnotes.cloudlink',
|
||||||
DailyDropboxBackup = 'org.standardnotes.daily-dropbox-backup',
|
DailyDropboxBackup = 'org.standardnotes.daily-dropbox-backup',
|
||||||
DailyEmailBackup = 'org.standardnotes.daily-email-backup',
|
DailyEmailBackup = 'org.standardnotes.daily-email-backup',
|
||||||
@@ -21,7 +20,6 @@ export enum FeatureIdentifier {
|
|||||||
AutobiographyTheme = 'org.standardnotes.theme-autobiography',
|
AutobiographyTheme = 'org.standardnotes.theme-autobiography',
|
||||||
DynamicTheme = 'org.standardnotes.theme-dynamic',
|
DynamicTheme = 'org.standardnotes.theme-dynamic',
|
||||||
DarkTheme = 'org.standardnotes.theme-focus',
|
DarkTheme = 'org.standardnotes.theme-focus',
|
||||||
FocusMode = 'org.standardnotes.focus-mode',
|
|
||||||
FuturaTheme = 'org.standardnotes.theme-futura',
|
FuturaTheme = 'org.standardnotes.theme-futura',
|
||||||
MidnightTheme = 'org.standardnotes.theme-midnight',
|
MidnightTheme = 'org.standardnotes.theme-midnight',
|
||||||
SolarizedDarkTheme = 'org.standardnotes.theme-solarized-dark',
|
SolarizedDarkTheme = 'org.standardnotes.theme-solarized-dark',
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { FeatureDescription } from './FeatureDescription'
|
import { FeatureDescription } from './FeatureDescription'
|
||||||
import { FeatureIdentifier } from './FeatureIdentifier'
|
import { FeatureIdentifier } from './FeatureIdentifier'
|
||||||
import { editors } from '../Lists/Editors'
|
|
||||||
import { themes } from '../Lists/Themes'
|
|
||||||
import { serverFeatures } from '../Lists/ServerFeatures'
|
import { serverFeatures } from '../Lists/ServerFeatures'
|
||||||
import { clientFeatures } from '../Lists/ClientFeatures'
|
import { clientFeatures } from '../Lists/ClientFeatures'
|
||||||
import { GetDeprecatedFeatures } from '../Lists/DeprecatedFeatures'
|
import { GetDeprecatedFeatures } from '../Lists/DeprecatedFeatures'
|
||||||
@@ -9,14 +7,7 @@ import { experimentalFeatures } from '../Lists/ExperimentalFeatures'
|
|||||||
import { SubscriptionName } from '@standardnotes/common'
|
import { SubscriptionName } from '@standardnotes/common'
|
||||||
|
|
||||||
export function GetFeatures(): FeatureDescription[] {
|
export function GetFeatures(): FeatureDescription[] {
|
||||||
return [
|
return [...serverFeatures(), ...clientFeatures(), ...experimentalFeatures(), ...GetDeprecatedFeatures()]
|
||||||
...themes(),
|
|
||||||
...editors(),
|
|
||||||
...serverFeatures(),
|
|
||||||
...clientFeatures(),
|
|
||||||
...experimentalFeatures(),
|
|
||||||
...GetDeprecatedFeatures(),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GetFeaturesForSubscription(subscription: SubscriptionName): FeatureDescription[] {
|
export function GetFeaturesForSubscription(subscription: SubscriptionName): FeatureDescription[] {
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
import { ClientFeatureDescription } from '../Feature/FeatureDescription'
|
import { FeatureDescription } from '../Feature/FeatureDescription'
|
||||||
import { PermissionName } from '../Permission/PermissionName'
|
import { PermissionName } from '../Permission/PermissionName'
|
||||||
import { FeatureIdentifier } from '../Feature/FeatureIdentifier'
|
import { FeatureIdentifier } from '../Feature/FeatureIdentifier'
|
||||||
import { SubscriptionName } from '@standardnotes/common'
|
import { SubscriptionName } from '@standardnotes/common'
|
||||||
import { RoleName } from '@standardnotes/domain-core'
|
import { RoleName } from '@standardnotes/domain-core'
|
||||||
|
import { themes } from './Themes'
|
||||||
|
import { editors } from './Editors'
|
||||||
|
|
||||||
export function clientFeatures(): ClientFeatureDescription[] {
|
export function clientFeatures(): FeatureDescription[] {
|
||||||
return [
|
return [
|
||||||
|
...themes(),
|
||||||
|
...editors(),
|
||||||
{
|
{
|
||||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
|
||||||
name: 'Tag Nesting',
|
name: 'Tag Nesting',
|
||||||
|
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
||||||
|
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||||
identifier: FeatureIdentifier.TagNesting,
|
identifier: FeatureIdentifier.TagNesting,
|
||||||
permission_name: PermissionName.TagNesting,
|
permission_name: PermissionName.TagNesting,
|
||||||
description: 'Organize your tags into folders.',
|
description: 'Organize your tags into folders.',
|
||||||
@@ -17,45 +22,26 @@ export function clientFeatures(): ClientFeatureDescription[] {
|
|||||||
name: 'Super Notes',
|
name: 'Super Notes',
|
||||||
identifier: FeatureIdentifier.SuperEditor,
|
identifier: FeatureIdentifier.SuperEditor,
|
||||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
||||||
|
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||||
permission_name: PermissionName.SuperEditor,
|
permission_name: PermissionName.SuperEditor,
|
||||||
description:
|
description:
|
||||||
'Type / to bring up the block selection menu, or @ to embed images or link other tags and notes. Type - then space to start a list, or [] then space to start a checklist. Drag and drop an image or file to embed it in your note.',
|
'A new way to edit notes. Type / to bring up the block selection menu, or @ to embed images or link other tags and notes. Type - then space to start a list, or [] then space to start a checklist. Drag and drop an image or file to embed it in your note. Cmd/Ctrl + F to bring up search and replace.',
|
||||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
|
||||||
name: 'Smart Filters',
|
name: 'Smart Filters',
|
||||||
|
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
||||||
|
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||||
identifier: FeatureIdentifier.SmartFilters,
|
identifier: FeatureIdentifier.SmartFilters,
|
||||||
permission_name: PermissionName.SmartFilters,
|
permission_name: PermissionName.SmartFilters,
|
||||||
description: 'Create smart filters for viewing notes matching specific criteria.',
|
description: 'Create smart filters for viewing notes matching specific criteria.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
|
||||||
name: 'Encrypted files',
|
name: 'Encrypted files',
|
||||||
|
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
||||||
|
availableInRoles: [RoleName.NAMES.ProUser],
|
||||||
identifier: FeatureIdentifier.Files,
|
identifier: FeatureIdentifier.Files,
|
||||||
permission_name: PermissionName.Files,
|
permission_name: PermissionName.Files,
|
||||||
description: '',
|
description: '',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
|
||||||
name: 'Focus Mode',
|
|
||||||
identifier: FeatureIdentifier.FocusMode,
|
|
||||||
permission_name: PermissionName.FocusMode,
|
|
||||||
description: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
|
||||||
name: 'Listed Custom Domain',
|
|
||||||
identifier: FeatureIdentifier.ListedCustomDomain,
|
|
||||||
permission_name: PermissionName.ListedCustomDomain,
|
|
||||||
description: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
|
||||||
name: 'Multiple accounts',
|
|
||||||
identifier: FeatureIdentifier.AccountSwitcher,
|
|
||||||
permission_name: PermissionName.AccountSwitcher,
|
|
||||||
description: '',
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { NoteType } from '../Component/NoteType'
|
|||||||
import { FillEditorComponentDefaults } from './Utilities/FillEditorComponentDefaults'
|
import { FillEditorComponentDefaults } from './Utilities/FillEditorComponentDefaults'
|
||||||
import { ComponentAction } from '../Component/ComponentAction'
|
import { ComponentAction } from '../Component/ComponentAction'
|
||||||
import { ComponentArea } from '../Component/ComponentArea'
|
import { ComponentArea } from '../Component/ComponentArea'
|
||||||
|
import { RoleName } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
export function GetDeprecatedFeatures(): FeatureDescription[] {
|
export function GetDeprecatedFeatures(): FeatureDescription[] {
|
||||||
const bold: EditorFeatureDescription = FillEditorComponentDefaults({
|
const bold: EditorFeatureDescription = FillEditorComponentDefaults({
|
||||||
@@ -37,6 +38,7 @@ export function GetDeprecatedFeatures(): FeatureDescription[] {
|
|||||||
permission_name: PermissionName.BoldEditor,
|
permission_name: PermissionName.BoldEditor,
|
||||||
description: 'A simple and peaceful rich editor that helps you write and think clearly.',
|
description: 'A simple and peaceful rich editor that helps you write and think clearly.',
|
||||||
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/bold.jpg',
|
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/bold.jpg',
|
||||||
|
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||||
})
|
})
|
||||||
|
|
||||||
const markdownBasic: EditorFeatureDescription = FillEditorComponentDefaults({
|
const markdownBasic: EditorFeatureDescription = FillEditorComponentDefaults({
|
||||||
@@ -50,6 +52,7 @@ export function GetDeprecatedFeatures(): FeatureDescription[] {
|
|||||||
permission_name: PermissionName.MarkdownBasicEditor,
|
permission_name: PermissionName.MarkdownBasicEditor,
|
||||||
description: 'A Markdown editor with dynamic split-pane preview.',
|
description: 'A Markdown editor with dynamic split-pane preview.',
|
||||||
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/simple-markdown.jpg',
|
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/simple-markdown.jpg',
|
||||||
|
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||||
})
|
})
|
||||||
|
|
||||||
const markdownMinimist: EditorFeatureDescription = FillEditorComponentDefaults({
|
const markdownMinimist: EditorFeatureDescription = FillEditorComponentDefaults({
|
||||||
@@ -64,6 +67,7 @@ export function GetDeprecatedFeatures(): FeatureDescription[] {
|
|||||||
deprecated: true,
|
deprecated: true,
|
||||||
description: 'A minimal Markdown editor with live rendering and in-text search via Ctrl/Cmd + F',
|
description: 'A minimal Markdown editor with live rendering and in-text search via Ctrl/Cmd + F',
|
||||||
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/min-markdown.jpg',
|
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/min-markdown.jpg',
|
||||||
|
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||||
})
|
})
|
||||||
|
|
||||||
const markdownMath: EditorFeatureDescription = FillEditorComponentDefaults({
|
const markdownMath: EditorFeatureDescription = FillEditorComponentDefaults({
|
||||||
@@ -78,6 +82,7 @@ export function GetDeprecatedFeatures(): FeatureDescription[] {
|
|||||||
index_path: 'index.html',
|
index_path: 'index.html',
|
||||||
description: 'A beautiful split-pane Markdown editor with synced-scroll, LaTeX support, and colorful syntax.',
|
description: 'A beautiful split-pane Markdown editor with synced-scroll, LaTeX support, and colorful syntax.',
|
||||||
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/fancy-markdown.jpg',
|
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/fancy-markdown.jpg',
|
||||||
|
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||||
})
|
})
|
||||||
|
|
||||||
const filesafe: IframeComponentFeatureDescription = FillEditorComponentDefaults({
|
const filesafe: IframeComponentFeatureDescription = FillEditorComponentDefaults({
|
||||||
@@ -104,6 +109,7 @@ export function GetDeprecatedFeatures(): FeatureDescription[] {
|
|||||||
description:
|
description:
|
||||||
'Encrypted attachments for your notes using your Dropbox, Google Drive, or WebDAV server. Limited to 50MB per file.',
|
'Encrypted attachments for your notes using your Dropbox, Google Drive, or WebDAV server. Limited to 50MB per file.',
|
||||||
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/FileSafe-banner.png',
|
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/FileSafe-banner.png',
|
||||||
|
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||||
})
|
})
|
||||||
|
|
||||||
return [bold, markdownBasic, markdownMinimist, markdownMath, filesafe]
|
return [bold, markdownBasic, markdownMinimist, markdownMath, filesafe]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { PermissionName } from '../Permission/PermissionName'
|
|||||||
import { FeatureIdentifier } from '../Feature/FeatureIdentifier'
|
import { FeatureIdentifier } from '../Feature/FeatureIdentifier'
|
||||||
import { NoteType } from '../Component/NoteType'
|
import { NoteType } from '../Component/NoteType'
|
||||||
import { FillEditorComponentDefaults } from './Utilities/FillEditorComponentDefaults'
|
import { FillEditorComponentDefaults } from './Utilities/FillEditorComponentDefaults'
|
||||||
|
import { RoleName } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
export function editors(): EditorFeatureDescription[] {
|
export function editors(): EditorFeatureDescription[] {
|
||||||
const code: EditorFeatureDescription = FillEditorComponentDefaults({
|
const code: EditorFeatureDescription = FillEditorComponentDefaults({
|
||||||
@@ -20,6 +21,7 @@ export function editors(): EditorFeatureDescription[] {
|
|||||||
'Syntax highlighting and convenient keyboard shortcuts for over 120 programming' +
|
'Syntax highlighting and convenient keyboard shortcuts for over 120 programming' +
|
||||||
' languages. Ideal for code snippets and procedures.',
|
' languages. Ideal for code snippets and procedures.',
|
||||||
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/code.jpg',
|
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/code.jpg',
|
||||||
|
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||||
})
|
})
|
||||||
|
|
||||||
const plus: EditorFeatureDescription = FillEditorComponentDefaults({
|
const plus: EditorFeatureDescription = FillEditorComponentDefaults({
|
||||||
@@ -33,6 +35,7 @@ export function editors(): EditorFeatureDescription[] {
|
|||||||
description:
|
description:
|
||||||
'From highlighting to custom font sizes and colors, to tables and lists, this editor is perfect for crafting any document.',
|
'From highlighting to custom font sizes and colors, to tables and lists, this editor is perfect for crafting any document.',
|
||||||
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/plus-editor.jpg',
|
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/plus-editor.jpg',
|
||||||
|
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||||
})
|
})
|
||||||
|
|
||||||
const markdown: EditorFeatureDescription = FillEditorComponentDefaults({
|
const markdown: EditorFeatureDescription = FillEditorComponentDefaults({
|
||||||
@@ -46,6 +49,7 @@ export function editors(): EditorFeatureDescription[] {
|
|||||||
description:
|
description:
|
||||||
'A fully featured Markdown editor that supports live preview, a styling toolbar, and split pane support.',
|
'A fully featured Markdown editor that supports live preview, a styling toolbar, and split pane support.',
|
||||||
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/adv-markdown.jpg',
|
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/adv-markdown.jpg',
|
||||||
|
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||||
})
|
})
|
||||||
|
|
||||||
const markdownAlt: EditorFeatureDescription = FillEditorComponentDefaults({
|
const markdownAlt: EditorFeatureDescription = FillEditorComponentDefaults({
|
||||||
@@ -59,6 +63,7 @@ export function editors(): EditorFeatureDescription[] {
|
|||||||
description:
|
description:
|
||||||
'A WYSIWYG-style Markdown editor that renders Markdown in preview-mode while you type without displaying any syntax.',
|
'A WYSIWYG-style Markdown editor that renders Markdown in preview-mode while you type without displaying any syntax.',
|
||||||
index_path: 'build/index.html',
|
index_path: 'build/index.html',
|
||||||
|
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||||
})
|
})
|
||||||
|
|
||||||
const task: EditorFeatureDescription = FillEditorComponentDefaults({
|
const task: EditorFeatureDescription = FillEditorComponentDefaults({
|
||||||
@@ -73,6 +78,7 @@ export function editors(): EditorFeatureDescription[] {
|
|||||||
description:
|
description:
|
||||||
'A great way to manage short-term and long-term to-do"s. You can mark tasks as completed, change their order, and edit the text naturally in place.',
|
'A great way to manage short-term and long-term to-do"s. You can mark tasks as completed, change their order, and edit the text naturally in place.',
|
||||||
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/task-editor.jpg',
|
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/task-editor.jpg',
|
||||||
|
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||||
})
|
})
|
||||||
|
|
||||||
const tokenvault: EditorFeatureDescription = FillEditorComponentDefaults({
|
const tokenvault: EditorFeatureDescription = FillEditorComponentDefaults({
|
||||||
@@ -86,6 +92,7 @@ export function editors(): EditorFeatureDescription[] {
|
|||||||
description:
|
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.',
|
'Encrypt and protect your 2FA secrets for all your internet accounts. Authenticator handles your 2FA secrets so that you never lose them again, or have to start over when you get a new device.',
|
||||||
thumbnail_url: 'https://standard-notes.s3.amazonaws.com/screenshots/models/editors/token-vault.png',
|
thumbnail_url: 'https://standard-notes.s3.amazonaws.com/screenshots/models/editors/token-vault.png',
|
||||||
|
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||||
})
|
})
|
||||||
|
|
||||||
const spreadsheets: EditorFeatureDescription = FillEditorComponentDefaults({
|
const spreadsheets: EditorFeatureDescription = FillEditorComponentDefaults({
|
||||||
@@ -99,6 +106,7 @@ export function editors(): EditorFeatureDescription[] {
|
|||||||
description:
|
description:
|
||||||
'A powerful spreadsheet editor with formatting and formula support. Not recommended for large data sets, as encryption of such data may decrease editor performance.',
|
'A powerful spreadsheet editor with formatting and formula support. Not recommended for large data sets, as encryption of such data may decrease editor performance.',
|
||||||
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/spreadsheets.png',
|
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/spreadsheets.png',
|
||||||
|
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||||
})
|
})
|
||||||
|
|
||||||
return [code, plus, markdown, markdownAlt, task, tokenvault, spreadsheets]
|
return [code, plus, markdown, markdownAlt, task, tokenvault, spreadsheets]
|
||||||
|
|||||||
@@ -65,5 +65,11 @@ export function serverFeatures(): ServerFeatureDescription[] {
|
|||||||
identifier: FeatureIdentifier.SubscriptionSharing,
|
identifier: FeatureIdentifier.SubscriptionSharing,
|
||||||
permission_name: PermissionName.SubscriptionSharing,
|
permission_name: PermissionName.SubscriptionSharing,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
||||||
|
name: 'Listed Custom Domain',
|
||||||
|
identifier: FeatureIdentifier.ListedCustomDomain,
|
||||||
|
permission_name: PermissionName.ListedCustomDomain,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { PermissionName } from '../Permission/PermissionName'
|
|||||||
import { FeatureIdentifier } from '../Feature/FeatureIdentifier'
|
import { FeatureIdentifier } from '../Feature/FeatureIdentifier'
|
||||||
import { FillThemeComponentDefaults } from './Utilities/FillThemeComponentDefaults'
|
import { FillThemeComponentDefaults } from './Utilities/FillThemeComponentDefaults'
|
||||||
import { SubscriptionName } from '@standardnotes/common'
|
import { SubscriptionName } from '@standardnotes/common'
|
||||||
|
import { RoleName } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
export function themes(): ThemeFeatureDescription[] {
|
export function themes(): ThemeFeatureDescription[] {
|
||||||
const midnight: ThemeFeatureDescription = FillThemeComponentDefaults({
|
const midnight: ThemeFeatureDescription = FillThemeComponentDefaults({
|
||||||
@@ -17,10 +18,12 @@ export function themes(): ThemeFeatureDescription[] {
|
|||||||
foreground_color: '#ffffff',
|
foreground_color: '#ffffff',
|
||||||
border_color: '#086DD6',
|
border_color: '#086DD6',
|
||||||
},
|
},
|
||||||
|
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||||
})
|
})
|
||||||
|
|
||||||
const futura: ThemeFeatureDescription = FillThemeComponentDefaults({
|
const futura: ThemeFeatureDescription = FillThemeComponentDefaults({
|
||||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
||||||
|
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||||
name: 'Futura',
|
name: 'Futura',
|
||||||
identifier: FeatureIdentifier.FuturaTheme,
|
identifier: FeatureIdentifier.FuturaTheme,
|
||||||
permission_name: PermissionName.FuturaTheme,
|
permission_name: PermissionName.FuturaTheme,
|
||||||
@@ -35,6 +38,7 @@ export function themes(): ThemeFeatureDescription[] {
|
|||||||
|
|
||||||
const solarizedDark: ThemeFeatureDescription = FillThemeComponentDefaults({
|
const solarizedDark: ThemeFeatureDescription = FillThemeComponentDefaults({
|
||||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
||||||
|
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||||
name: 'Solarized Dark',
|
name: 'Solarized Dark',
|
||||||
identifier: FeatureIdentifier.SolarizedDarkTheme,
|
identifier: FeatureIdentifier.SolarizedDarkTheme,
|
||||||
permission_name: PermissionName.SolarizedDarkTheme,
|
permission_name: PermissionName.SolarizedDarkTheme,
|
||||||
@@ -49,6 +53,7 @@ export function themes(): ThemeFeatureDescription[] {
|
|||||||
|
|
||||||
const autobiography: ThemeFeatureDescription = FillThemeComponentDefaults({
|
const autobiography: ThemeFeatureDescription = FillThemeComponentDefaults({
|
||||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
||||||
|
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||||
name: 'Autobiography',
|
name: 'Autobiography',
|
||||||
identifier: FeatureIdentifier.AutobiographyTheme,
|
identifier: FeatureIdentifier.AutobiographyTheme,
|
||||||
permission_name: PermissionName.AutobiographyTheme,
|
permission_name: PermissionName.AutobiographyTheme,
|
||||||
@@ -77,6 +82,7 @@ export function themes(): ThemeFeatureDescription[] {
|
|||||||
|
|
||||||
const titanium: ThemeFeatureDescription = FillThemeComponentDefaults({
|
const titanium: ThemeFeatureDescription = FillThemeComponentDefaults({
|
||||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
||||||
|
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||||
name: 'Titanium',
|
name: 'Titanium',
|
||||||
identifier: FeatureIdentifier.TitaniumTheme,
|
identifier: FeatureIdentifier.TitaniumTheme,
|
||||||
permission_name: PermissionName.TitaniumTheme,
|
permission_name: PermissionName.TitaniumTheme,
|
||||||
@@ -90,6 +96,7 @@ export function themes(): ThemeFeatureDescription[] {
|
|||||||
|
|
||||||
const dynamic: ThemeFeatureDescription = FillThemeComponentDefaults({
|
const dynamic: ThemeFeatureDescription = FillThemeComponentDefaults({
|
||||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
||||||
|
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||||
name: 'Dynamic Panels',
|
name: 'Dynamic Panels',
|
||||||
identifier: FeatureIdentifier.DynamicTheme,
|
identifier: FeatureIdentifier.DynamicTheme,
|
||||||
permission_name: PermissionName.ThemeDynamic,
|
permission_name: PermissionName.ThemeDynamic,
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ import { MinimalHttpResponse } from '../Http/MinimalHttpResponses'
|
|||||||
export type GetOfflineFeaturesResponse = MinimalHttpResponse & {
|
export type GetOfflineFeaturesResponse = MinimalHttpResponse & {
|
||||||
data?: {
|
data?: {
|
||||||
features: FeatureDescription[]
|
features: FeatureDescription[]
|
||||||
|
roles: string[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FeatureDescription, FeatureIdentifier } from '@standardnotes/features'
|
import { FeatureIdentifier } from '@standardnotes/features'
|
||||||
import { SNComponent } from '@standardnotes/models'
|
import { SNComponent } from '@standardnotes/models'
|
||||||
|
|
||||||
import { FeatureStatus } from './FeatureStatus'
|
import { FeatureStatus } from './FeatureStatus'
|
||||||
@@ -7,10 +7,10 @@ import { SetOfflineFeaturesFunctionResponse } from './SetOfflineFeaturesFunction
|
|||||||
export interface FeaturesClientInterface {
|
export interface FeaturesClientInterface {
|
||||||
downloadExternalFeature(urlOrCode: string): Promise<SNComponent | undefined>
|
downloadExternalFeature(urlOrCode: string): Promise<SNComponent | undefined>
|
||||||
|
|
||||||
getUserFeature(featureId: FeatureIdentifier): FeatureDescription | undefined
|
|
||||||
|
|
||||||
getFeatureStatus(featureId: FeatureIdentifier): FeatureStatus
|
getFeatureStatus(featureId: FeatureIdentifier): FeatureStatus
|
||||||
|
|
||||||
|
hasFirstPartySubscription(): boolean
|
||||||
|
|
||||||
hasMinimumRole(role: string): boolean
|
hasMinimumRole(role: string): boolean
|
||||||
|
|
||||||
setOfflineFeaturesCode(code: string): Promise<SetOfflineFeaturesFunctionResponse>
|
setOfflineFeaturesCode(code: string): Promise<SetOfflineFeaturesFunctionResponse>
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export enum StorageKey {
|
|||||||
StorageEncryptionPolicy = 'storage_policy',
|
StorageEncryptionPolicy = 'storage_policy',
|
||||||
WebSocketUrl = 'webSocket_url',
|
WebSocketUrl = 'webSocket_url',
|
||||||
UserRoles = 'user_roles',
|
UserRoles = 'user_roles',
|
||||||
|
OfflineUserRoles = 'offline_user_roles',
|
||||||
UserFeatures = 'user_features',
|
UserFeatures = 'user_features',
|
||||||
ExperimentalFeatures = 'experimental_features',
|
ExperimentalFeatures = 'experimental_features',
|
||||||
DeinitMode = 'deinit_mode',
|
DeinitMode = 'deinit_mode',
|
||||||
|
|||||||
@@ -1160,8 +1160,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
|||||||
return this.apiService.isThirdPartyHostUsed()
|
return this.apiService.isThirdPartyHostUsed()
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCloudProviderIntegrationUrl(cloudProviderName: Settings.CloudProvider, isDevEnvironment: boolean): string {
|
public getCloudProviderIntegrationUrl(cloudProviderName: Settings.CloudProvider): string {
|
||||||
return this.settingsService.getCloudProviderIntegrationUrl(cloudProviderName, isDevEnvironment)
|
return this.settingsService.getCloudProviderIntegrationUrl(cloudProviderName)
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructServices() {
|
private constructServices() {
|
||||||
|
|||||||
@@ -1,24 +1,15 @@
|
|||||||
export const APPLICATION_DEFAULT_HOSTS = [
|
export const APPLICATION_DEFAULT_HOSTS = ['api.standardnotes.com', 'sync.standardnotes.org']
|
||||||
'api.standardnotes.com',
|
|
||||||
'api-dev.standardnotes.com',
|
|
||||||
'sync.standardnotes.org',
|
|
||||||
'syncing-server-demo.standardnotes.com',
|
|
||||||
]
|
|
||||||
|
|
||||||
export const FILES_DEFAULT_HOSTS = ['files.standardnotes.com', 'files-dev.standardnotes.com']
|
export const FILES_DEFAULT_HOSTS = ['files.standardnotes.com']
|
||||||
|
|
||||||
export const TRUSTED_FEATURE_HOSTS = [
|
export const TRUSTED_FEATURE_HOSTS = [
|
||||||
'api-dev.standardnotes.com',
|
|
||||||
'api.standardnotes.com',
|
'api.standardnotes.com',
|
||||||
'extensions.standardnotes.com',
|
'extensions.standardnotes.com',
|
||||||
'extensions.standardnotes.org',
|
'extensions.standardnotes.org',
|
||||||
'extensions-server-dev.standardnotes.org',
|
|
||||||
'extensions-server-dev.standardnotes.com',
|
|
||||||
'features.standardnotes.com',
|
'features.standardnotes.com',
|
||||||
]
|
]
|
||||||
|
|
||||||
export enum ExtensionsServerURL {
|
export enum ExtensionsServerURL {
|
||||||
Dev = 'https://extensions-server-dev.standardnotes.org',
|
|
||||||
Prod = 'https://extensions.standardnotes.org',
|
Prod = 'https://extensions.standardnotes.org',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -654,7 +654,7 @@ export class SNApiService
|
|||||||
|
|
||||||
public async downloadOfflineFeaturesFromRepo(
|
public async downloadOfflineFeaturesFromRepo(
|
||||||
repo: SNFeatureRepo,
|
repo: SNFeatureRepo,
|
||||||
): Promise<{ features: FeatureDescription[] } | ClientDisplayableError> {
|
): Promise<{ features: FeatureDescription[]; roles: string[] } | ClientDisplayableError> {
|
||||||
try {
|
try {
|
||||||
const featuresUrl = repo.offlineFeaturesUrl
|
const featuresUrl = repo.offlineFeaturesUrl
|
||||||
const extensionKey = repo.offlineKey
|
const extensionKey = repo.offlineKey
|
||||||
@@ -678,8 +678,10 @@ export class SNApiService
|
|||||||
if (response.error) {
|
if (response.error) {
|
||||||
return ClientDisplayableError.FromError(response.error)
|
return ClientDisplayableError.FromError(response.error)
|
||||||
}
|
}
|
||||||
|
const data = (response as Responses.GetOfflineFeaturesResponse).data
|
||||||
return {
|
return {
|
||||||
features: (response as Responses.GetOfflineFeaturesResponse).data?.features || [],
|
features: data?.features || [],
|
||||||
|
roles: data?.roles || [],
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return new ClientDisplayableError(API_MESSAGE_FAILED_OFFLINE_ACTIVATION)
|
return new ClientDisplayableError(API_MESSAGE_FAILED_OFFLINE_ACTIVATION)
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export class SNWebSocketsService extends AbstractService<WebSocketsServiceEvent,
|
|||||||
|
|
||||||
return response.data.token
|
return response.data.token
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error((error as Error).message)
|
console.error('Caught error:', (error as Error).message)
|
||||||
|
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ describe('featuresService', () => {
|
|||||||
featuresService.getExperimentalFeatures = jest.fn().mockReturnValue([FeatureIdentifier.PlusEditor])
|
featuresService.getExperimentalFeatures = jest.fn().mockReturnValue([FeatureIdentifier.PlusEditor])
|
||||||
|
|
||||||
featuresService.initializeFromDisk()
|
featuresService.initializeFromDisk()
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
|
||||||
expect(itemManager.createItem).not.toHaveBeenCalled()
|
expect(itemManager.createItem).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -191,7 +191,7 @@ describe('featuresService', () => {
|
|||||||
featuresService.getEnabledExperimentalFeatures = jest.fn().mockReturnValue([FeatureIdentifier.PlusEditor])
|
featuresService.getEnabledExperimentalFeatures = jest.fn().mockReturnValue([FeatureIdentifier.PlusEditor])
|
||||||
|
|
||||||
featuresService.initializeFromDisk()
|
featuresService.initializeFromDisk()
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
|
||||||
expect(itemManager.createItem).toHaveBeenCalled()
|
expect(itemManager.createItem).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -213,7 +213,7 @@ describe('featuresService', () => {
|
|||||||
const mock = (featuresService['notifyEvent'] = jest.fn())
|
const mock = (featuresService['notifyEvent'] = jest.fn())
|
||||||
|
|
||||||
const newRoles = [...roles, RoleName.NAMES.PlusUser]
|
const newRoles = [...roles, RoleName.NAMES.PlusUser]
|
||||||
await featuresService.setRoles(newRoles)
|
await featuresService.setOnlineRoles(newRoles)
|
||||||
|
|
||||||
expect(mock.mock.calls[0][0]).toEqual(FeaturesEvent.UserRolesChanged)
|
expect(mock.mock.calls[0][0]).toEqual(FeaturesEvent.UserRolesChanged)
|
||||||
})
|
})
|
||||||
@@ -226,7 +226,7 @@ describe('featuresService', () => {
|
|||||||
const spy = jest.spyOn(featuresService, 'notifyEvent' as never)
|
const spy = jest.spyOn(featuresService, 'notifyEvent' as never)
|
||||||
|
|
||||||
const newRoles = [...roles, RoleName.NAMES.ProUser]
|
const newRoles = [...roles, RoleName.NAMES.ProUser]
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
|
||||||
|
|
||||||
expect(spy.mock.calls[2][0]).toEqual(FeaturesEvent.DidPurchaseSubscription)
|
expect(spy.mock.calls[2][0]).toEqual(FeaturesEvent.DidPurchaseSubscription)
|
||||||
})
|
})
|
||||||
@@ -235,12 +235,12 @@ describe('featuresService', () => {
|
|||||||
storageService.getValue = jest.fn().mockReturnValue(roles)
|
storageService.getValue = jest.fn().mockReturnValue(roles)
|
||||||
const featuresService = createService()
|
const featuresService = createService()
|
||||||
featuresService.initializeFromDisk()
|
featuresService.initializeFromDisk()
|
||||||
featuresService['roles'] = []
|
featuresService['onlineRoles'] = []
|
||||||
|
|
||||||
const spy = jest.spyOn(featuresService, 'notifyEvent' as never)
|
const spy = jest.spyOn(featuresService, 'notifyEvent' as never)
|
||||||
|
|
||||||
const newRoles = [...roles, RoleName.NAMES.ProUser]
|
const newRoles = [...roles, RoleName.NAMES.ProUser]
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
|
||||||
|
|
||||||
const triggeredEvents = spy.mock.calls.map((call) => call[0])
|
const triggeredEvents = spy.mock.calls.map((call) => call[0])
|
||||||
expect(triggeredEvents).not.toContain(FeaturesEvent.DidPurchaseSubscription)
|
expect(triggeredEvents).not.toContain(FeaturesEvent.DidPurchaseSubscription)
|
||||||
@@ -252,7 +252,7 @@ describe('featuresService', () => {
|
|||||||
storageService.getValue = jest.fn().mockReturnValue(roles)
|
storageService.getValue = jest.fn().mockReturnValue(roles)
|
||||||
const featuresService = createService()
|
const featuresService = createService()
|
||||||
featuresService.initializeFromDisk()
|
featuresService.initializeFromDisk()
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
|
||||||
expect(storageService.setValue).toHaveBeenCalledWith(StorageKey.UserRoles, newRoles)
|
expect(storageService.setValue).toHaveBeenCalledWith(StorageKey.UserRoles, newRoles)
|
||||||
expect(apiService.getUserFeatures).toHaveBeenCalledWith('123')
|
expect(apiService.getUserFeatures).toHaveBeenCalledWith('123')
|
||||||
})
|
})
|
||||||
@@ -263,7 +263,7 @@ describe('featuresService', () => {
|
|||||||
storageService.getValue = jest.fn().mockReturnValue(roles)
|
storageService.getValue = jest.fn().mockReturnValue(roles)
|
||||||
const featuresService = createService()
|
const featuresService = createService()
|
||||||
featuresService.initializeFromDisk()
|
featuresService.initializeFromDisk()
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
|
||||||
expect(storageService.setValue).toHaveBeenCalledWith(StorageKey.UserRoles, newRoles)
|
expect(storageService.setValue).toHaveBeenCalledWith(StorageKey.UserRoles, newRoles)
|
||||||
expect(apiService.getUserFeatures).toHaveBeenCalledWith('123')
|
expect(apiService.getUserFeatures).toHaveBeenCalledWith('123')
|
||||||
})
|
})
|
||||||
@@ -274,7 +274,7 @@ describe('featuresService', () => {
|
|||||||
storageService.getValue = jest.fn().mockReturnValue(roles)
|
storageService.getValue = jest.fn().mockReturnValue(roles)
|
||||||
const featuresService = createService()
|
const featuresService = createService()
|
||||||
featuresService.initializeFromDisk()
|
featuresService.initializeFromDisk()
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
|
||||||
expect(storageService.setValue).toHaveBeenCalledWith(StorageKey.UserFeatures, features)
|
expect(storageService.setValue).toHaveBeenCalledWith(StorageKey.UserFeatures, features)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -284,7 +284,7 @@ describe('featuresService', () => {
|
|||||||
storageService.getValue = jest.fn().mockReturnValue(roles)
|
storageService.getValue = jest.fn().mockReturnValue(roles)
|
||||||
const featuresService = createService()
|
const featuresService = createService()
|
||||||
featuresService.initializeFromDisk()
|
featuresService.initializeFromDisk()
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
|
||||||
expect(itemManager.createItem).toHaveBeenCalledTimes(2)
|
expect(itemManager.createItem).toHaveBeenCalledTimes(2)
|
||||||
expect(itemManager.createItem).toHaveBeenCalledWith(
|
expect(itemManager.createItem).toHaveBeenCalledWith(
|
||||||
ContentType.Theme,
|
ContentType.Theme,
|
||||||
@@ -328,7 +328,7 @@ describe('featuresService', () => {
|
|||||||
itemManager.getItems = jest.fn().mockReturnValue([existingItem])
|
itemManager.getItems = jest.fn().mockReturnValue([existingItem])
|
||||||
const featuresService = createService()
|
const featuresService = createService()
|
||||||
featuresService.initializeFromDisk()
|
featuresService.initializeFromDisk()
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
|
||||||
|
|
||||||
expect(itemManager.changeComponent).toHaveBeenCalledWith(existingItem, expect.any(Function))
|
expect(itemManager.changeComponent).toHaveBeenCalledWith(existingItem, expect.any(Function))
|
||||||
})
|
})
|
||||||
@@ -354,7 +354,7 @@ describe('featuresService', () => {
|
|||||||
|
|
||||||
const featuresService = createService()
|
const featuresService = createService()
|
||||||
featuresService.initializeFromDisk()
|
featuresService.initializeFromDisk()
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
|
||||||
expect(itemManager.createItem).toHaveBeenCalledWith(
|
expect(itemManager.createItem).toHaveBeenCalledWith(
|
||||||
ContentType.Component,
|
ContentType.Component,
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -401,7 +401,7 @@ describe('featuresService', () => {
|
|||||||
|
|
||||||
const featuresService = createService()
|
const featuresService = createService()
|
||||||
featuresService.initializeFromDisk()
|
featuresService.initializeFromDisk()
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
|
||||||
expect(itemManager.setItemsToBeDeleted).toHaveBeenCalledWith([existingItem])
|
expect(itemManager.setItemsToBeDeleted).toHaveBeenCalledWith([existingItem])
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -424,7 +424,7 @@ describe('featuresService', () => {
|
|||||||
storageService.getValue = jest.fn().mockReturnValue(roles)
|
storageService.getValue = jest.fn().mockReturnValue(roles)
|
||||||
const featuresService = createService()
|
const featuresService = createService()
|
||||||
featuresService.initializeFromDisk()
|
featuresService.initializeFromDisk()
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
|
||||||
expect(itemManager.createItem).not.toHaveBeenCalled()
|
expect(itemManager.createItem).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -447,7 +447,7 @@ describe('featuresService', () => {
|
|||||||
storageService.getValue = jest.fn().mockReturnValue(roles)
|
storageService.getValue = jest.fn().mockReturnValue(roles)
|
||||||
const featuresService = createService()
|
const featuresService = createService()
|
||||||
featuresService.initializeFromDisk()
|
featuresService.initializeFromDisk()
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
|
||||||
expect(itemManager.createItem).not.toHaveBeenCalled()
|
expect(itemManager.createItem).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -455,10 +455,10 @@ describe('featuresService', () => {
|
|||||||
storageService.getValue = jest.fn().mockReturnValue(roles)
|
storageService.getValue = jest.fn().mockReturnValue(roles)
|
||||||
const featuresService = createService()
|
const featuresService = createService()
|
||||||
featuresService.initializeFromDisk()
|
featuresService.initializeFromDisk()
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', roles)
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', roles)
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', roles)
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', roles)
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', roles)
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', roles)
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', roles)
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', roles)
|
||||||
expect(storageService.setValue).toHaveBeenCalledTimes(2)
|
expect(storageService.setValue).toHaveBeenCalledTimes(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -482,7 +482,7 @@ describe('featuresService', () => {
|
|||||||
const nativeFeature = featuresService['mapRemoteNativeFeatureToStaticFeature'](remoteFeature)
|
const nativeFeature = featuresService['mapRemoteNativeFeatureToStaticFeature'](remoteFeature)
|
||||||
featuresService['mapRemoteNativeFeatureToItem'] = jest.fn()
|
featuresService['mapRemoteNativeFeatureToItem'] = jest.fn()
|
||||||
featuresService.initializeFromDisk()
|
featuresService.initializeFromDisk()
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
|
||||||
expect(featuresService['mapRemoteNativeFeatureToItem']).toHaveBeenCalledWith(
|
expect(featuresService['mapRemoteNativeFeatureToItem']).toHaveBeenCalledWith(
|
||||||
nativeFeature,
|
nativeFeature,
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
@@ -509,7 +509,26 @@ describe('featuresService', () => {
|
|||||||
await expect(() => featuresService['mapRemoteNativeFeatureToItem'](clientFeature, [], [])).rejects.toThrow()
|
await expect(() => featuresService['mapRemoteNativeFeatureToItem'](clientFeature, [], [])).rejects.toThrow()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('feature status', async () => {
|
it('role-based feature status', async () => {
|
||||||
|
const featuresService = createService()
|
||||||
|
|
||||||
|
features = [] as jest.Mocked<FeatureDescription[]>
|
||||||
|
|
||||||
|
apiService.getUserFeatures = jest.fn().mockReturnValue({
|
||||||
|
data: {
|
||||||
|
features,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
|
||||||
|
|
||||||
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
||||||
|
|
||||||
|
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled)
|
||||||
|
expect(featuresService.getFeatureStatus(FeatureIdentifier.SuperEditor)).toBe(FeatureStatus.Entitled)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('feature status with no paid role but features listings', async () => {
|
||||||
const featuresService = createService()
|
const featuresService = createService()
|
||||||
|
|
||||||
features = [
|
features = [
|
||||||
@@ -535,54 +554,21 @@ describe('featuresService', () => {
|
|||||||
|
|
||||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
|
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
|
||||||
|
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
|
||||||
|
|
||||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled)
|
|
||||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.PlusEditor)).toBe(FeatureStatus.NotInCurrentPlan)
|
|
||||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.SheetsEditor)).toBe(FeatureStatus.NotInCurrentPlan)
|
|
||||||
|
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
|
|
||||||
|
|
||||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.NoUserSubscription)
|
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.NoUserSubscription)
|
||||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.PlusEditor)).toBe(FeatureStatus.NoUserSubscription)
|
expect(featuresService.getFeatureStatus(FeatureIdentifier.PlusEditor)).toBe(FeatureStatus.NoUserSubscription)
|
||||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.SheetsEditor)).toBe(FeatureStatus.NoUserSubscription)
|
expect(featuresService.getFeatureStatus(FeatureIdentifier.SheetsEditor)).toBe(FeatureStatus.NoUserSubscription)
|
||||||
|
|
||||||
features = [
|
|
||||||
{
|
|
||||||
identifier: FeatureIdentifier.MidnightTheme,
|
|
||||||
content_type: ContentType.Theme,
|
|
||||||
expires_at: expiredDate,
|
|
||||||
role_name: RoleName.NAMES.PlusUser,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
identifier: FeatureIdentifier.PlusEditor,
|
|
||||||
content_type: ContentType.Component,
|
|
||||||
expires_at: expiredDate,
|
|
||||||
role_name: RoleName.NAMES.ProUser,
|
|
||||||
},
|
|
||||||
] as jest.Mocked<FeatureDescription[]>
|
|
||||||
|
|
||||||
apiService.getUserFeatures = jest.fn().mockReturnValue({
|
|
||||||
data: {
|
|
||||||
features,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.PlusUser])
|
|
||||||
|
|
||||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(
|
|
||||||
FeatureStatus.InCurrentPlanButExpired,
|
|
||||||
)
|
|
||||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.PlusEditor)).toBe(FeatureStatus.NotInCurrentPlan)
|
|
||||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.SheetsEditor)).toBe(FeatureStatus.NotInCurrentPlan)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('availableInRoles-based features', async () => {
|
it('role-based features while not signed into first party server', async () => {
|
||||||
const featuresService = createService()
|
const featuresService = createService()
|
||||||
|
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.ProUser])
|
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false)
|
||||||
|
|
||||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.SuperEditor)).toBe(FeatureStatus.Entitled)
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.ProUser])
|
||||||
|
|
||||||
|
expect(featuresService.getFeatureStatus(FeatureIdentifier.SuperEditor)).toBe(FeatureStatus.NotInCurrentPlan)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('third party feature status', async () => {
|
it('third party feature status', async () => {
|
||||||
@@ -629,7 +615,7 @@ describe('featuresService', () => {
|
|||||||
} as never),
|
} as never),
|
||||||
])
|
])
|
||||||
|
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
|
||||||
|
|
||||||
expect(featuresService.getFeatureStatus(themeFeature.identifier)).toBe(FeatureStatus.Entitled)
|
expect(featuresService.getFeatureStatus(themeFeature.identifier)).toBe(FeatureStatus.Entitled)
|
||||||
expect(featuresService.getFeatureStatus(editorFeature.identifier)).toBe(FeatureStatus.InCurrentPlanButExpired)
|
expect(featuresService.getFeatureStatus(editorFeature.identifier)).toBe(FeatureStatus.InCurrentPlanButExpired)
|
||||||
@@ -641,7 +627,7 @@ describe('featuresService', () => {
|
|||||||
it('feature status should be not entitled if no account or offline repo', async () => {
|
it('feature status should be not entitled if no account or offline repo', async () => {
|
||||||
const featuresService = createService()
|
const featuresService = createService()
|
||||||
|
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
|
||||||
|
|
||||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false)
|
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false)
|
||||||
|
|
||||||
@@ -653,30 +639,6 @@ describe('featuresService', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('feature status should be entitled for subscriber until first successful features request made if no cached features', async () => {
|
|
||||||
const featuresService = createService()
|
|
||||||
|
|
||||||
apiService.getUserFeatures = jest.fn().mockReturnValue({
|
|
||||||
data: {
|
|
||||||
features: [],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
|
||||||
|
|
||||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
|
|
||||||
|
|
||||||
featuresService['completedSuccessfulFeaturesRetrieval'] = false
|
|
||||||
|
|
||||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled)
|
|
||||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.TokenVaultEditor)).toBe(FeatureStatus.Entitled)
|
|
||||||
|
|
||||||
await featuresService.didDownloadFeatures(features)
|
|
||||||
|
|
||||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled)
|
|
||||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.TokenVaultEditor)).toBe(FeatureStatus.NotInCurrentPlan)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('didDownloadFeatures should filter out client controlled features', async () => {
|
it('didDownloadFeatures should filter out client controlled features', async () => {
|
||||||
const featuresService = createService()
|
const featuresService = createService()
|
||||||
|
|
||||||
@@ -687,31 +649,13 @@ describe('featuresService', () => {
|
|||||||
expect(featuresService['mapRemoteNativeFeaturesToItems']).toHaveBeenCalledWith([])
|
expect(featuresService['mapRemoteNativeFeaturesToItems']).toHaveBeenCalledWith([])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('feature status should be dynamic for subscriber if cached features and no successful features request made yet', async () => {
|
|
||||||
const featuresService = createService()
|
|
||||||
|
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
|
||||||
|
|
||||||
featuresService['completedSuccessfulFeaturesRetrieval'] = false
|
|
||||||
|
|
||||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
|
|
||||||
|
|
||||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled)
|
|
||||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.TokenVaultEditor)).toBe(FeatureStatus.NotInCurrentPlan)
|
|
||||||
|
|
||||||
featuresService['completedSuccessfulFeaturesRetrieval'] = false
|
|
||||||
|
|
||||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled)
|
|
||||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.TokenVaultEditor)).toBe(FeatureStatus.NotInCurrentPlan)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('feature status for offline subscription', async () => {
|
it('feature status for offline subscription', async () => {
|
||||||
const featuresService = createService()
|
const featuresService = createService()
|
||||||
|
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
||||||
|
|
||||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false)
|
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false)
|
||||||
featuresService.rolesIncludePaidSubscription = jest.fn().mockReturnValue(false)
|
featuresService.onlineRolesIncludePaidSubscription = jest.fn().mockReturnValue(false)
|
||||||
featuresService['completedSuccessfulFeaturesRetrieval'] = true
|
featuresService['completedSuccessfulFeaturesRetrieval'] = true
|
||||||
|
|
||||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.NoUserSubscription)
|
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.NoUserSubscription)
|
||||||
@@ -720,9 +664,11 @@ describe('featuresService', () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
featuresService.hasOfflineRepo = jest.fn().mockReturnValue(true)
|
featuresService.hasOfflineRepo = jest.fn().mockReturnValue(true)
|
||||||
|
featuresService.hasFirstPartySubscription = jest.fn().mockReturnValue(true)
|
||||||
|
await featuresService.setOfflineRoles([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
||||||
|
|
||||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled)
|
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled)
|
||||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.TokenVaultEditor)).toBe(FeatureStatus.NotInCurrentPlan)
|
expect(featuresService.getFeatureStatus(FeatureIdentifier.TokenVaultEditor)).toBe(FeatureStatus.Entitled)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('feature status for deprecated feature', async () => {
|
it('feature status for deprecated feature', async () => {
|
||||||
@@ -734,7 +680,7 @@ describe('featuresService', () => {
|
|||||||
FeatureStatus.NoUserSubscription,
|
FeatureStatus.NoUserSubscription,
|
||||||
)
|
)
|
||||||
|
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
||||||
|
|
||||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.DeprecatedFileSafe as FeatureIdentifier)).toBe(
|
expect(featuresService.getFeatureStatus(FeatureIdentifier.DeprecatedFileSafe as FeatureIdentifier)).toBe(
|
||||||
FeatureStatus.Entitled,
|
FeatureStatus.Entitled,
|
||||||
@@ -744,25 +690,25 @@ describe('featuresService', () => {
|
|||||||
it('has paid subscription', async () => {
|
it('has paid subscription', async () => {
|
||||||
const featuresService = createService()
|
const featuresService = createService()
|
||||||
|
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
|
||||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
|
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
|
||||||
|
|
||||||
expect(featuresService.hasPaidOnlineOrOfflineSubscription()).toBeFalsy
|
expect(featuresService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toBeFalsy
|
||||||
|
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
||||||
|
|
||||||
expect(featuresService.hasPaidOnlineOrOfflineSubscription()).toEqual(true)
|
expect(featuresService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('has paid subscription should be true if offline repo and signed into third party server', async () => {
|
it('has paid subscription should be true if offline repo and signed into third party server', async () => {
|
||||||
const featuresService = createService()
|
const featuresService = createService()
|
||||||
|
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
|
||||||
|
|
||||||
featuresService.hasOfflineRepo = jest.fn().mockReturnValue(true)
|
featuresService.hasOfflineRepo = jest.fn().mockReturnValue(true)
|
||||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false)
|
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false)
|
||||||
|
|
||||||
expect(featuresService.hasPaidOnlineOrOfflineSubscription()).toEqual(true)
|
expect(featuresService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -829,7 +775,11 @@ describe('featuresService', () => {
|
|||||||
it('should sort given roles according to role hierarchy', () => {
|
it('should sort given roles according to role hierarchy', () => {
|
||||||
const featuresService = createService()
|
const featuresService = createService()
|
||||||
|
|
||||||
const sortedRoles = featuresService.rolesBySorting([RoleName.NAMES.ProUser, RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
const sortedRoles = featuresService.rolesBySorting([
|
||||||
|
RoleName.NAMES.ProUser,
|
||||||
|
RoleName.NAMES.CoreUser,
|
||||||
|
RoleName.NAMES.PlusUser,
|
||||||
|
])
|
||||||
|
|
||||||
expect(sortedRoles).toStrictEqual([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser])
|
expect(sortedRoles).toStrictEqual([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser])
|
||||||
})
|
})
|
||||||
@@ -839,7 +789,7 @@ describe('featuresService', () => {
|
|||||||
it('should be false if core user checks for plus role', async () => {
|
it('should be false if core user checks for plus role', async () => {
|
||||||
const featuresService = createService()
|
const featuresService = createService()
|
||||||
|
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
|
||||||
|
|
||||||
const hasPlusUserRole = featuresService.hasMinimumRole(RoleName.NAMES.PlusUser)
|
const hasPlusUserRole = featuresService.hasMinimumRole(RoleName.NAMES.PlusUser)
|
||||||
|
|
||||||
@@ -849,7 +799,9 @@ describe('featuresService', () => {
|
|||||||
it('should be false if plus user checks for pro role', async () => {
|
it('should be false if plus user checks for pro role', async () => {
|
||||||
const featuresService = createService()
|
const featuresService = createService()
|
||||||
|
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.PlusUser, RoleName.NAMES.CoreUser])
|
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
|
||||||
|
|
||||||
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.PlusUser, RoleName.NAMES.CoreUser])
|
||||||
|
|
||||||
const hasProUserRole = featuresService.hasMinimumRole(RoleName.NAMES.ProUser)
|
const hasProUserRole = featuresService.hasMinimumRole(RoleName.NAMES.ProUser)
|
||||||
|
|
||||||
@@ -859,7 +811,9 @@ describe('featuresService', () => {
|
|||||||
it('should be true if pro user checks for core user', async () => {
|
it('should be true if pro user checks for core user', async () => {
|
||||||
const featuresService = createService()
|
const featuresService = createService()
|
||||||
|
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser])
|
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
|
||||||
|
|
||||||
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser])
|
||||||
|
|
||||||
const hasCoreUserRole = featuresService.hasMinimumRole(RoleName.NAMES.CoreUser)
|
const hasCoreUserRole = featuresService.hasMinimumRole(RoleName.NAMES.CoreUser)
|
||||||
|
|
||||||
@@ -869,7 +823,9 @@ describe('featuresService', () => {
|
|||||||
it('should be true if pro user checks for pro user', async () => {
|
it('should be true if pro user checks for pro user', async () => {
|
||||||
const featuresService = createService()
|
const featuresService = createService()
|
||||||
|
|
||||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser])
|
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
|
||||||
|
|
||||||
|
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser])
|
||||||
|
|
||||||
const hasProUserRole = featuresService.hasMinimumRole(RoleName.NAMES.ProUser)
|
const hasProUserRole = featuresService.hasMinimumRole(RoleName.NAMES.ProUser)
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ export class SNFeaturesService
|
|||||||
implements FeaturesClientInterface, InternalEventHandlerInterface
|
implements FeaturesClientInterface, InternalEventHandlerInterface
|
||||||
{
|
{
|
||||||
private deinited = false
|
private deinited = false
|
||||||
private roles: string[] = []
|
private onlineRoles: string[] = []
|
||||||
|
private offlineRoles: string[] = []
|
||||||
private features: FeaturesImports.FeatureDescription[] = []
|
private features: FeaturesImports.FeatureDescription[] = []
|
||||||
private enabledExperimentalFeatures: FeaturesImports.FeatureIdentifier[] = []
|
private enabledExperimentalFeatures: FeaturesImports.FeatureIdentifier[] = []
|
||||||
private removeWebSocketsServiceObserver: () => void
|
private removeWebSocketsServiceObserver: () => void
|
||||||
@@ -87,7 +88,7 @@ export class SNFeaturesService
|
|||||||
const {
|
const {
|
||||||
payload: { userUuid, currentRoles },
|
payload: { userUuid, currentRoles },
|
||||||
} = data as UserRolesChangedEvent
|
} = data as UserRolesChangedEvent
|
||||||
await this.updateRolesAndFetchFeatures(userUuid, currentRoles)
|
await this.updateOnlineRolesAndFetchFeatures(userUuid, currentRoles)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -124,6 +125,16 @@ export class SNFeaturesService
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public initializeFromDisk(): void {
|
||||||
|
this.onlineRoles = this.storageService.getValue<string[]>(StorageKey.UserRoles, undefined, [])
|
||||||
|
|
||||||
|
this.offlineRoles = this.storageService.getValue<string[]>(StorageKey.OfflineUserRoles, undefined, [])
|
||||||
|
|
||||||
|
this.features = this.storageService.getValue(StorageKey.UserFeatures, undefined, [])
|
||||||
|
|
||||||
|
this.enabledExperimentalFeatures = this.storageService.getValue(StorageKey.ExperimentalFeatures, undefined, [])
|
||||||
|
}
|
||||||
|
|
||||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||||
if (event.type === ApiServiceEvent.MetaReceived) {
|
if (event.type === ApiServiceEvent.MetaReceived) {
|
||||||
if (!this.syncService) {
|
if (!this.syncService) {
|
||||||
@@ -142,7 +153,7 @@ export class SNFeaturesService
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { userUuid, userRoles } = event.payload as MetaReceivedData
|
const { userUuid, userRoles } = event.payload as MetaReceivedData
|
||||||
await this.updateRolesAndFetchFeatures(
|
await this.updateOnlineRolesAndFetchFeatures(
|
||||||
userUuid,
|
userUuid,
|
||||||
userRoles.map((role) => role.name),
|
userRoles.map((role) => role.name),
|
||||||
)
|
)
|
||||||
@@ -155,7 +166,7 @@ export class SNFeaturesService
|
|||||||
if (stage === ApplicationStage.FullSyncCompleted_13) {
|
if (stage === ApplicationStage.FullSyncCompleted_13) {
|
||||||
void this.mapClientControlledFeaturesToItems()
|
void this.mapClientControlledFeaturesToItems()
|
||||||
|
|
||||||
if (!this.rolesIncludePaidSubscription()) {
|
if (!this.hasFirstPartyOnlineSubscription()) {
|
||||||
const offlineRepo = this.getOfflineRepo()
|
const offlineRepo = this.getOfflineRepo()
|
||||||
if (offlineRepo) {
|
if (offlineRepo) {
|
||||||
void this.downloadOfflineFeatures(offlineRepo)
|
void this.downloadOfflineFeatures(offlineRepo)
|
||||||
@@ -194,7 +205,7 @@ export class SNFeaturesService
|
|||||||
}
|
}
|
||||||
|
|
||||||
public enableExperimentalFeature(identifier: FeaturesImports.FeatureIdentifier): void {
|
public enableExperimentalFeature(identifier: FeaturesImports.FeatureIdentifier): void {
|
||||||
const feature = this.getUserFeature(identifier)
|
const feature = this.getFeatureThatOriginallyCameFromServer(identifier)
|
||||||
|
|
||||||
this.enabledExperimentalFeatures.push(identifier)
|
this.enabledExperimentalFeatures.push(identifier)
|
||||||
|
|
||||||
@@ -309,10 +320,14 @@ export class SNFeaturesService
|
|||||||
repo: Models.SNFeatureRepo,
|
repo: Models.SNFeatureRepo,
|
||||||
): Promise<SetOfflineFeaturesFunctionResponse | ClientDisplayableError> {
|
): Promise<SetOfflineFeaturesFunctionResponse | ClientDisplayableError> {
|
||||||
const result = await this.apiService.downloadOfflineFeaturesFromRepo(repo)
|
const result = await this.apiService.downloadOfflineFeaturesFromRepo(repo)
|
||||||
|
|
||||||
if (result instanceof ClientDisplayableError) {
|
if (result instanceof ClientDisplayableError) {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.didDownloadFeatures(result.features)
|
await this.didDownloadFeatures(result.features)
|
||||||
|
await this.setOfflineRoles(result.roles)
|
||||||
|
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,18 +378,29 @@ export class SNFeaturesService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public initializeFromDisk(): void {
|
hasFirstPartyOnlineSubscription(): boolean {
|
||||||
this.roles = this.storageService.getValue<string[]>(StorageKey.UserRoles, undefined, [])
|
return this.sessionManager.isSignedIntoFirstPartyServer() && this.onlineRolesIncludePaidSubscription()
|
||||||
|
|
||||||
this.features = this.storageService.getValue(StorageKey.UserFeatures, undefined, [])
|
|
||||||
|
|
||||||
this.enabledExperimentalFeatures = this.storageService.getValue(StorageKey.ExperimentalFeatures, undefined, [])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateRolesAndFetchFeatures(userUuid: UuidString, roles: string[]): Promise<void> {
|
hasFirstPartySubscription(): boolean {
|
||||||
const previousRoles = this.roles
|
if (this.hasFirstPartyOnlineSubscription()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
const userRolesChanged = this.haveRolesChanged(roles)
|
const offlineRepo = this.getOfflineRepo()
|
||||||
|
if (!offlineRepo) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasFirstPartyOfflineSubscription = offlineRepo.content.offlineFeaturesUrl === PROD_OFFLINE_FEATURES_URL
|
||||||
|
return hasFirstPartyOfflineSubscription
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateOnlineRolesAndFetchFeatures(userUuid: UuidString, roles: string[]): Promise<void> {
|
||||||
|
const previousRoles = this.onlineRoles
|
||||||
|
|
||||||
|
const userRolesChanged =
|
||||||
|
roles.some((role) => !this.onlineRoles.includes(role)) || this.onlineRoles.some((role) => !roles.includes(role))
|
||||||
|
|
||||||
const isInitialLoadRolesChange = previousRoles.length === 0 && userRolesChanged
|
const isInitialLoadRolesChange = previousRoles.length === 0 && userRolesChanged
|
||||||
|
|
||||||
@@ -384,7 +410,7 @@ export class SNFeaturesService
|
|||||||
|
|
||||||
this.needsInitialFeaturesUpdate = false
|
this.needsInitialFeaturesUpdate = false
|
||||||
|
|
||||||
await this.setRoles(roles)
|
await this.setOnlineRoles(roles)
|
||||||
|
|
||||||
const shouldDownloadRoleBasedFeatures = !this.hasOfflineRepo()
|
const shouldDownloadRoleBasedFeatures = !this.hasOfflineRepo()
|
||||||
|
|
||||||
@@ -398,22 +424,34 @@ export class SNFeaturesService
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (userRolesChanged && !isInitialLoadRolesChange) {
|
if (userRolesChanged && !isInitialLoadRolesChange) {
|
||||||
if (this.rolesIncludePaidSubscription()) {
|
if (this.onlineRolesIncludePaidSubscription()) {
|
||||||
await this.notifyEvent(FeaturesEvent.DidPurchaseSubscription)
|
await this.notifyEvent(FeaturesEvent.DidPurchaseSubscription)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setRoles(roles: string[]): Promise<void> {
|
async setOnlineRoles(roles: string[]): Promise<void> {
|
||||||
const rolesChanged = !arraysEqual(this.roles, roles)
|
const rolesChanged = !arraysEqual(this.onlineRoles, roles)
|
||||||
|
|
||||||
this.roles = roles
|
this.onlineRoles = roles
|
||||||
|
|
||||||
if (rolesChanged) {
|
if (rolesChanged) {
|
||||||
void this.notifyEvent(FeaturesEvent.UserRolesChanged)
|
void this.notifyEvent(FeaturesEvent.UserRolesChanged)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.storageService.setValue(StorageKey.UserRoles, this.roles)
|
this.storageService.setValue(StorageKey.UserRoles, this.onlineRoles)
|
||||||
|
}
|
||||||
|
|
||||||
|
async setOfflineRoles(roles: string[]): Promise<void> {
|
||||||
|
const rolesChanged = !arraysEqual(this.offlineRoles, roles)
|
||||||
|
|
||||||
|
this.offlineRoles = roles
|
||||||
|
|
||||||
|
if (rolesChanged) {
|
||||||
|
void this.notifyEvent(FeaturesEvent.UserRolesChanged)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.storageService.setValue(StorageKey.OfflineUserRoles, this.offlineRoles)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async didDownloadFeatures(features: FeaturesImports.FeatureDescription[]): Promise<void> {
|
public async didDownloadFeatures(features: FeaturesImports.FeatureDescription[]): Promise<void> {
|
||||||
@@ -465,17 +503,19 @@ export class SNFeaturesService
|
|||||||
return nativeFeatureCopy
|
return nativeFeatureCopy
|
||||||
}
|
}
|
||||||
|
|
||||||
public getUserFeature(featureId: FeaturesImports.FeatureIdentifier): FeaturesImports.FeatureDescription | undefined {
|
public getFeatureThatOriginallyCameFromServer(
|
||||||
|
featureId: FeaturesImports.FeatureIdentifier,
|
||||||
|
): FeaturesImports.FeatureDescription | undefined {
|
||||||
return this.features.find((feature) => feature.identifier === featureId)
|
return this.features.find((feature) => feature.identifier === featureId)
|
||||||
}
|
}
|
||||||
|
|
||||||
rolesIncludePaidSubscription(): boolean {
|
onlineRolesIncludePaidSubscription(): boolean {
|
||||||
const unpaidRoles = [RoleName.NAMES.CoreUser]
|
const unpaidRoles = [RoleName.NAMES.CoreUser]
|
||||||
return this.roles.some((role) => !unpaidRoles.includes(role))
|
return this.onlineRoles.some((role) => !unpaidRoles.includes(role))
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasPaidOnlineOrOfflineSubscription(): boolean {
|
hasPaidAnyPartyOnlineOrOfflineSubscription(): boolean {
|
||||||
return this.rolesIncludePaidSubscription() || this.hasOfflineRepo()
|
return this.onlineRolesIncludePaidSubscription() || this.hasOfflineRepo()
|
||||||
}
|
}
|
||||||
|
|
||||||
public rolesBySorting(roles: string[]): string[] {
|
public rolesBySorting(roles: string[]): string[] {
|
||||||
@@ -485,7 +525,7 @@ export class SNFeaturesService
|
|||||||
public hasMinimumRole(role: string): boolean {
|
public hasMinimumRole(role: string): boolean {
|
||||||
const sortedAllRoles = Object.values(RoleName.NAMES)
|
const sortedAllRoles = Object.values(RoleName.NAMES)
|
||||||
|
|
||||||
const sortedUserRoles = this.rolesBySorting(this.roles)
|
const sortedUserRoles = this.rolesBySorting(this.rolesToUseForFeatureCheck())
|
||||||
|
|
||||||
const highestUserRoleIndex = sortedAllRoles.indexOf(lastElement(sortedUserRoles) as string)
|
const highestUserRoleIndex = sortedAllRoles.indexOf(lastElement(sortedUserRoles) as string)
|
||||||
|
|
||||||
@@ -508,16 +548,10 @@ export class SNFeaturesService
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nativeFeature = FeaturesImports.FindNativeFeature(featureId)
|
const nativeFeature = FeaturesImports.FindNativeFeature(featureId)
|
||||||
if (nativeFeature && nativeFeature.availableInRoles) {
|
|
||||||
const hasRole = this.roles.some((role) => nativeFeature.availableInRoles?.includes(role))
|
|
||||||
if (hasRole) {
|
|
||||||
return FeatureStatus.Entitled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isDeprecated = this.isFeatureDeprecated(featureId)
|
const isDeprecated = this.isFeatureDeprecated(featureId)
|
||||||
if (isDeprecated) {
|
if (isDeprecated) {
|
||||||
if (this.hasPaidOnlineOrOfflineSubscription()) {
|
if (this.hasPaidAnyPartyOnlineOrOfflineSubscription()) {
|
||||||
return FeatureStatus.Entitled
|
return FeatureStatus.Entitled
|
||||||
} else {
|
} else {
|
||||||
return FeatureStatus.NoUserSubscription
|
return FeatureStatus.NoUserSubscription
|
||||||
@@ -538,7 +572,7 @@ export class SNFeaturesService
|
|||||||
return FeatureStatus.Entitled
|
return FeatureStatus.Entitled
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.hasPaidOnlineOrOfflineSubscription()) {
|
if (this.hasPaidAnyPartyOnlineOrOfflineSubscription()) {
|
||||||
if (!this.completedSuccessfulFeaturesRetrieval) {
|
if (!this.completedSuccessfulFeaturesRetrieval) {
|
||||||
const hasCachedFeatures = this.features.length > 0
|
const hasCachedFeatures = this.features.length > 0
|
||||||
const temporarilyAllowUntilServerUpdates = !hasCachedFeatures
|
const temporarilyAllowUntilServerUpdates = !hasCachedFeatures
|
||||||
@@ -550,25 +584,27 @@ export class SNFeaturesService
|
|||||||
return FeatureStatus.NoUserSubscription
|
return FeatureStatus.NoUserSubscription
|
||||||
}
|
}
|
||||||
|
|
||||||
const feature = this.getUserFeature(featureId)
|
if (nativeFeature) {
|
||||||
if (!feature) {
|
if (!this.hasFirstPartySubscription()) {
|
||||||
return FeatureStatus.NotInCurrentPlan
|
|
||||||
}
|
|
||||||
|
|
||||||
const expired = feature.expires_at && new Date(feature.expires_at).getTime() < new Date().getTime()
|
|
||||||
if (expired) {
|
|
||||||
if (!this.roles.includes(feature.role_name as string)) {
|
|
||||||
return FeatureStatus.NotInCurrentPlan
|
return FeatureStatus.NotInCurrentPlan
|
||||||
} else {
|
}
|
||||||
return FeatureStatus.InCurrentPlanButExpired
|
|
||||||
|
const roles = this.rolesToUseForFeatureCheck()
|
||||||
|
if (nativeFeature.availableInRoles) {
|
||||||
|
const hasRole = roles.some((role) => {
|
||||||
|
return nativeFeature.availableInRoles?.includes(role)
|
||||||
|
})
|
||||||
|
if (!hasRole) {
|
||||||
|
return FeatureStatus.NotInCurrentPlan
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return FeatureStatus.Entitled
|
return FeatureStatus.Entitled
|
||||||
}
|
}
|
||||||
|
|
||||||
private haveRolesChanged(roles: string[]): boolean {
|
private rolesToUseForFeatureCheck(): string[] {
|
||||||
return roles.some((role) => !this.roles.includes(role)) || this.roles.some((role) => !roles.includes(role))
|
return this.hasFirstPartyOnlineSubscription() ? this.onlineRoles : this.offlineRoles
|
||||||
}
|
}
|
||||||
|
|
||||||
private componentContentForNativeFeatureDescription(feature: FeaturesImports.FeatureDescription): Models.ItemContent {
|
private componentContentForNativeFeatureDescription(feature: FeaturesImports.FeatureDescription): Models.ItemContent {
|
||||||
@@ -776,7 +812,8 @@ export class SNFeaturesService
|
|||||||
;(this.removeWebSocketsServiceObserver as unknown) = undefined
|
;(this.removeWebSocketsServiceObserver as unknown) = undefined
|
||||||
this.removefeatureReposObserver()
|
this.removefeatureReposObserver()
|
||||||
;(this.removefeatureReposObserver as unknown) = undefined
|
;(this.removefeatureReposObserver as unknown) = undefined
|
||||||
;(this.roles as unknown) = undefined
|
;(this.onlineRoles as unknown) = undefined
|
||||||
|
;(this.offlineRoles as unknown) = undefined
|
||||||
;(this.storageService as unknown) = undefined
|
;(this.storageService as unknown) = undefined
|
||||||
;(this.apiService as unknown) = undefined
|
;(this.apiService as unknown) = undefined
|
||||||
;(this.itemManager as unknown) = undefined
|
;(this.itemManager as unknown) = undefined
|
||||||
@@ -793,7 +830,7 @@ export class SNFeaturesService
|
|||||||
override getDiagnostics(): Promise<DiagnosticInfo | undefined> {
|
override getDiagnostics(): Promise<DiagnosticInfo | undefined> {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
features: {
|
features: {
|
||||||
roles: this.roles,
|
roles: this.onlineRoles,
|
||||||
features: this.features,
|
features: this.features,
|
||||||
enabledExperimentalFeatures: this.enabledExperimentalFeatures,
|
enabledExperimentalFeatures: this.enabledExperimentalFeatures,
|
||||||
needsInitialFeaturesUpdate: this.needsInitialFeaturesUpdate,
|
needsInitialFeaturesUpdate: this.needsInitialFeaturesUpdate,
|
||||||
|
|||||||
@@ -66,10 +66,9 @@ export class SNSettingsService extends AbstractService implements SettingsClient
|
|||||||
return this.frequencyOptionsLabels[frequency]
|
return this.frequencyOptionsLabels[frequency]
|
||||||
}
|
}
|
||||||
|
|
||||||
getCloudProviderIntegrationUrl(cloudProviderName: CloudProvider, isDevEnvironment: boolean): string {
|
getCloudProviderIntegrationUrl(cloudProviderName: CloudProvider): string {
|
||||||
const { Dev, Prod } = ExtensionsServerURL
|
const { Prod } = ExtensionsServerURL
|
||||||
const extServerUrl = isDevEnvironment ? Dev : Prod
|
return `${Prod}/${this.cloudProviderIntegrationUrlEndpoints[cloudProviderName]}?redirect_url=${Prod}/components/cloudlink?`
|
||||||
return `${extServerUrl}/${this.cloudProviderIntegrationUrlEndpoints[cloudProviderName]}?redirect_url=${extServerUrl}/components/cloudlink?`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override deinit(): void {
|
override deinit(): void {
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ describe('features', () => {
|
|||||||
|
|
||||||
describe('new user roles received on api response meta', () => {
|
describe('new user roles received on api response meta', () => {
|
||||||
it('should save roles and features', async () => {
|
it('should save roles and features', async () => {
|
||||||
expect(application.featuresService.roles).to.have.lengthOf(1)
|
expect(application.featuresService.onlineRoles).to.have.lengthOf(1)
|
||||||
expect(application.featuresService.roles[0]).to.equal('CORE_USER')
|
expect(application.featuresService.onlineRoles[0]).to.equal('CORE_USER')
|
||||||
|
|
||||||
expect(application.featuresService.features).to.have.lengthOf(3)
|
expect(application.featuresService.features).to.have.lengthOf(3)
|
||||||
expect(application.featuresService.features[0]).to.containSubset(midnightThemeFeature)
|
expect(application.featuresService.features[0]).to.containSubset(midnightThemeFeature)
|
||||||
@@ -115,7 +115,7 @@ describe('features', () => {
|
|||||||
// Wipe items from initial sync
|
// Wipe items from initial sync
|
||||||
await application.itemManager.removeAllItemsFromMemory()
|
await application.itemManager.removeAllItemsFromMemory()
|
||||||
// Wipe roles from initial sync
|
// Wipe roles from initial sync
|
||||||
await application.featuresService.setRoles([])
|
await application.featuresService.setOnlineRoles([])
|
||||||
// Create pre-existing item for theme without all the info
|
// Create pre-existing item for theme without all the info
|
||||||
await application.itemManager.createItem(
|
await application.itemManager.createItem(
|
||||||
ContentType.Theme,
|
ContentType.Theme,
|
||||||
@@ -165,7 +165,7 @@ describe('features', () => {
|
|||||||
.find((theme) => theme.identifier === midnightThemeFeature.identifier)
|
.find((theme) => theme.identifier === midnightThemeFeature.identifier)
|
||||||
|
|
||||||
// Wipe roles from initial sync
|
// Wipe roles from initial sync
|
||||||
await application.featuresService.setRoles([])
|
await application.featuresService.setOnlineRoles([])
|
||||||
|
|
||||||
// Call sync intentionally to get roles again in meta
|
// Call sync intentionally to get roles again in meta
|
||||||
await application.sync.sync()
|
await application.sync.sync()
|
||||||
@@ -184,7 +184,7 @@ describe('features', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should provide feature', async () => {
|
it('should provide feature', async () => {
|
||||||
const feature = application.features.getUserFeature(FeatureIdentifier.PlusEditor)
|
const feature = application.features.getFeatureThatOriginallyCameFromServer(FeatureIdentifier.PlusEditor)
|
||||||
expect(feature).to.containSubset(plusEditorFeature)
|
expect(feature).to.containSubset(plusEditorFeature)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -645,6 +645,8 @@ describe('keys', function () {
|
|||||||
|
|
||||||
expect(Object.keys(clientBUndecryptables).length).to.equal(1)
|
expect(Object.keys(clientBUndecryptables).length).to.equal(1)
|
||||||
expect(Object.keys(clientAUndecryptables).length).to.equal(0)
|
expect(Object.keys(clientAUndecryptables).length).to.equal(0)
|
||||||
|
|
||||||
|
await contextB.deinit()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('changing password on 003 client while signed into 004 client should', function () {
|
describe('changing password on 003 client while signed into 004 client should', function () {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ describe('online conflict handling', function () {
|
|||||||
|
|
||||||
afterEach(async function () {
|
afterEach(async function () {
|
||||||
if (!this.application.dealloced) {
|
if (!this.application.dealloced) {
|
||||||
await Factory.safeDeinit(this.application)
|
await this.context.deinit()
|
||||||
}
|
}
|
||||||
localStorage.clear()
|
localStorage.clear()
|
||||||
})
|
})
|
||||||
@@ -950,6 +950,7 @@ describe('online conflict handling', function () {
|
|||||||
expect(contextA.findNoteByTitle('title-B').payload.updated_at_timestamp).to.equal(noteBExpectedTimestamp)
|
expect(contextA.findNoteByTitle('title-B').payload.updated_at_timestamp).to.equal(noteBExpectedTimestamp)
|
||||||
|
|
||||||
await this.sharedFinalAssertions()
|
await this.sharedFinalAssertions()
|
||||||
|
await contextB.deinit()
|
||||||
}).timeout(20000)
|
}).timeout(20000)
|
||||||
|
|
||||||
it('editing original note many times after conflict on other client should only result in 2 cumulative notes', async function () {
|
it('editing original note many times after conflict on other client should only result in 2 cumulative notes', async function () {
|
||||||
@@ -979,5 +980,6 @@ describe('online conflict handling', function () {
|
|||||||
expect(contextB.noteCount).to.equal(2)
|
expect(contextB.noteCount).to.equal(2)
|
||||||
|
|
||||||
await this.sharedFinalAssertions()
|
await this.sharedFinalAssertions()
|
||||||
|
await contextB.deinit()
|
||||||
}).timeout(20000)
|
}).timeout(20000)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1052,5 +1052,7 @@ describe('online syncing', function () {
|
|||||||
await contextB.sync()
|
await contextB.sync()
|
||||||
|
|
||||||
expect(contextB.application.items.allCountableNotesCount()).to.equal(0)
|
expect(contextB.application.items.allCountableNotesCount()).to.equal(0)
|
||||||
|
|
||||||
|
await contextB.deinit()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
|
|||||||
### Features
|
### Features
|
||||||
|
|
||||||
* Added rename option to file preview modal ([aa88966](https://github.com/standardnotes/app/commit/aa8896678315de67551c786e64aec7dfd10479e3))
|
* Added rename option to file preview modal ([aa88966](https://github.com/standardnotes/app/commit/aa8896678315de67551c786e64aec7dfd10479e3))
|
||||||
* **snjs:** add revisions api v2 ([#2154](https://github.com/standardnotes/app/issues/2154)) ([880a537](https://github.com/standardnotes/app/commit/880a537774ddcefaedb0d4e5dc50b363f4b93e01))
|
|
||||||
|
|
||||||
## [3.138.6](https://github.com/standardnotes/app/compare/@standardnotes/web@3.138.5...@standardnotes/web@3.138.6) (2023-01-17)
|
## [3.138.6](https://github.com/standardnotes/app/compare/@standardnotes/web@3.138.5...@standardnotes/web@3.138.6) (2023-01-17)
|
||||||
|
|
||||||
|
|||||||
@@ -50,12 +50,10 @@
|
|||||||
"body": "### Features\n\n* Added rename option to file preview modal ([aa88966](https://github.com/standardnotes/app/commit/aa8896678315de67551c786e64aec7dfd10479e3))\n* **snjs:** add revisions api v2 ([#2154](https://github.com/standardnotes/app/issues/2154)) ([880a537](https://github.com/standardnotes/app/commit/880a537774ddcefaedb0d4e5dc50b363f4b93e01))",
|
"body": "### Features\n\n* Added rename option to file preview modal ([aa88966](https://github.com/standardnotes/app/commit/aa8896678315de67551c786e64aec7dfd10479e3))\n* **snjs:** add revisions api v2 ([#2154](https://github.com/standardnotes/app/issues/2154)) ([880a537](https://github.com/standardnotes/app/commit/880a537774ddcefaedb0d4e5dc50b363f4b93e01))",
|
||||||
"parsed": {
|
"parsed": {
|
||||||
"_": [
|
"_": [
|
||||||
"Added rename option to file preview modal (aa88966)",
|
"Added rename option to file preview modal (aa88966)"
|
||||||
"snjs: add revisions api v2 (#2154) (880a537)"
|
|
||||||
],
|
],
|
||||||
"Features": [
|
"Features": [
|
||||||
"Added rename option to file preview modal (aa88966)",
|
"Added rename option to file preview modal (aa88966)"
|
||||||
"snjs: add revisions api v2 (#2154) (880a537)"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type Props = {
|
|||||||
const UpgradeNow = ({ application, featuresController, subscriptionContoller }: Props) => {
|
const UpgradeNow = ({ application, featuresController, subscriptionContoller }: Props) => {
|
||||||
const shouldShowCTA = !featuresController.hasFolders
|
const shouldShowCTA = !featuresController.hasFolders
|
||||||
const hasAccount = subscriptionContoller.hasAccount
|
const hasAccount = subscriptionContoller.hasAccount
|
||||||
|
const hasAccessToFeatures = subscriptionContoller.hasFirstPartySubscription
|
||||||
|
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
if (hasAccount && application.isNativeIOS()) {
|
if (hasAccount && application.isNativeIOS()) {
|
||||||
@@ -22,16 +23,20 @@ const UpgradeNow = ({ application, featuresController, subscriptionContoller }:
|
|||||||
}
|
}
|
||||||
}, [application, hasAccount])
|
}, [application, hasAccount])
|
||||||
|
|
||||||
return shouldShowCTA ? (
|
if (!shouldShowCTA || hasAccessToFeatures) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="flex h-full items-center px-2">
|
<div className="flex h-full items-center px-2">
|
||||||
<button
|
<button
|
||||||
className="rounded bg-info py-0.5 px-1.5 text-sm font-bold uppercase text-info-contrast hover:brightness-125 lg:text-xs"
|
className="rounded bg-info py-0.5 px-1.5 text-sm font-bold uppercase text-info-contrast hover:brightness-125 lg:text-xs"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
{hasAccount ? 'Unlock features' : 'Sign up to sync'}
|
{!hasAccount ? 'Sign up to sync' : 'Unlock features'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : null
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default observer(UpgradeNow)
|
export default observer(UpgradeNow)
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ const Email: FunctionComponent<Props> = ({ application }: Props) => {
|
|||||||
<Subtitle>Disable sign-in notification emails</Subtitle>
|
<Subtitle>Disable sign-in notification emails</Subtitle>
|
||||||
<Text>
|
<Text>
|
||||||
Disables email notifications when a new sign-in occurs on your account. (Email notifications are
|
Disables email notifications when a new sign-in occurs on your account. (Email notifications are
|
||||||
available to paid subscribers).
|
available only to paid subscribers).
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ type Props = {
|
|||||||
|
|
||||||
const Subscription: FunctionComponent<Props> = ({ application, viewControllerManager }: Props) => {
|
const Subscription: FunctionComponent<Props> = ({ application, viewControllerManager }: Props) => {
|
||||||
const subscriptionState = viewControllerManager.subscriptionController
|
const subscriptionState = viewControllerManager.subscriptionController
|
||||||
const { userSubscription } = subscriptionState
|
const { onlineSubscription } = subscriptionState
|
||||||
|
|
||||||
const now = new Date().getTime()
|
const now = new Date().getTime()
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ const Subscription: FunctionComponent<Props> = ({ application, viewControllerMan
|
|||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
<div className="flex flex-grow flex-col">
|
<div className="flex flex-grow flex-col">
|
||||||
<Title>Subscription</Title>
|
<Title>Subscription</Title>
|
||||||
{userSubscription && userSubscription.endsAt > now ? (
|
{onlineSubscription && onlineSubscription.endsAt > now ? (
|
||||||
<SubscriptionInformation subscriptionState={subscriptionState} application={application} />
|
<SubscriptionInformation subscriptionState={subscriptionState} application={application} />
|
||||||
) : (
|
) : (
|
||||||
<NoSubscription application={application} />
|
<NoSubscription application={application} />
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
import Button from '@/Components/Button/Button'
|
import Button from '@/Components/Button/Button'
|
||||||
import { isDev, openInNewTab } from '@/Utils'
|
import { openInNewTab } from '@/Utils'
|
||||||
import { Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
|
import { Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ const CloudBackupProvider: FunctionComponent<Props> = ({ application, providerNa
|
|||||||
}
|
}
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
||||||
const authUrl = application.getCloudProviderIntegrationUrl(providerName, isDev)
|
const authUrl = application.getCloudProviderIntegrationUrl(providerName)
|
||||||
openInNewTab(authUrl)
|
openInNewTab(authUrl)
|
||||||
setAuthBegan(true)
|
setAuthBegan(true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export class PackageProvider {
|
|||||||
static async load(application: WebApplication): Promise<PackageProvider | undefined> {
|
static async load(application: WebApplication): Promise<PackageProvider | undefined> {
|
||||||
const response = await application.getAvailableSubscriptions()
|
const response = await application.getAvailableSubscriptions()
|
||||||
|
|
||||||
if (response instanceof ClientDisplayableError) {
|
if (!response || response instanceof ClientDisplayableError) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,10 @@ export const UpgradePrompt = ({
|
|||||||
<div className="mb-2 font-bold">The Professional Plan costs $119.99/year and includes benefits like</div>
|
<div className="mb-2 font-bold">The Professional Plan costs $119.99/year and includes benefits like</div>
|
||||||
<ul className="list-inside list-[circle]">
|
<ul className="list-inside list-[circle]">
|
||||||
<li>100GB encrypted file storage</li>
|
<li>100GB encrypted file storage</li>
|
||||||
<li>Access to all note types, including markdown, rich text, authenticator, tasks, and spreadsheets</li>
|
<li>
|
||||||
|
Access to all note types, including Super, markdown, rich text, authenticator, tasks, and spreadsheets
|
||||||
|
</li>
|
||||||
|
<li>Access to Daily Notebooks and Moments journals</li>
|
||||||
<li>Note history going back indefinitely</li>
|
<li>Note history going back indefinitely</li>
|
||||||
<li>Nested folders for your tags</li>
|
<li>Nested folders for your tags</li>
|
||||||
<li>Premium support</li>
|
<li>Premium support</li>
|
||||||
@@ -79,7 +82,7 @@ export const UpgradePrompt = ({
|
|||||||
className="no-border w-full cursor-pointer rounded bg-info py-2 font-bold text-info-contrast hover:brightness-125 focus:brightness-125"
|
className="no-border w-full cursor-pointer rounded bg-info py-2 font-bold text-info-contrast hover:brightness-125 focus:brightness-125"
|
||||||
ref={ctaRef}
|
ref={ctaRef}
|
||||||
>
|
>
|
||||||
Upgrade
|
{application.isNativeIOS() ? 'Start Free Trial' : 'Upgrade'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export class FeaturesController extends AbstractViewController {
|
|||||||
break
|
break
|
||||||
case ApplicationEvent.FeaturesUpdated:
|
case ApplicationEvent.FeaturesUpdated:
|
||||||
case ApplicationEvent.Launched:
|
case ApplicationEvent.Launched:
|
||||||
|
case ApplicationEvent.LocalDataLoaded:
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.hasFolders = this.isEntitledToFolders()
|
this.hasFolders = this.isEntitledToFolders()
|
||||||
this.hasSmartViews = this.isEntitledToSmartViews()
|
this.hasSmartViews = this.isEntitledToSmartViews()
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export class LinkingController extends AbstractViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get isEntitledToNoteLinking() {
|
get isEntitledToNoteLinking() {
|
||||||
return !!this.subscriptionController.userSubscription
|
return !!this.subscriptionController.onlineSubscription
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLinkingPanelOpen = (open: boolean) => {
|
setIsLinkingPanelOpen = (open: boolean) => {
|
||||||
|
|||||||
@@ -17,14 +17,15 @@ import { Subscription } from './SubscriptionType'
|
|||||||
export class SubscriptionController extends AbstractViewController {
|
export class SubscriptionController extends AbstractViewController {
|
||||||
private readonly ALLOWED_SUBSCRIPTION_INVITATIONS = 5
|
private readonly ALLOWED_SUBSCRIPTION_INVITATIONS = 5
|
||||||
|
|
||||||
userSubscription: Subscription | undefined = undefined
|
onlineSubscription: Subscription | undefined = undefined
|
||||||
availableSubscriptions: AvailableSubscriptions | undefined = undefined
|
availableSubscriptions: AvailableSubscriptions | undefined = undefined
|
||||||
subscriptionInvitations: Invitation[] | undefined = undefined
|
subscriptionInvitations: Invitation[] | undefined = undefined
|
||||||
hasAccount: boolean
|
hasAccount: boolean
|
||||||
|
hasFirstPartySubscription: boolean
|
||||||
|
|
||||||
override deinit() {
|
override deinit() {
|
||||||
super.deinit()
|
super.deinit()
|
||||||
;(this.userSubscription as unknown) = undefined
|
;(this.onlineSubscription as unknown) = undefined
|
||||||
;(this.availableSubscriptions as unknown) = undefined
|
;(this.availableSubscriptions as unknown) = undefined
|
||||||
;(this.subscriptionInvitations as unknown) = undefined
|
;(this.subscriptionInvitations as unknown) = undefined
|
||||||
|
|
||||||
@@ -38,12 +39,14 @@ export class SubscriptionController extends AbstractViewController {
|
|||||||
) {
|
) {
|
||||||
super(application, eventBus)
|
super(application, eventBus)
|
||||||
this.hasAccount = application.hasAccount()
|
this.hasAccount = application.hasAccount()
|
||||||
|
this.hasFirstPartySubscription = application.features.hasFirstPartySubscription()
|
||||||
|
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
userSubscription: observable,
|
onlineSubscription: observable,
|
||||||
availableSubscriptions: observable,
|
availableSubscriptions: observable,
|
||||||
subscriptionInvitations: observable,
|
subscriptionInvitations: observable,
|
||||||
hasAccount: observable,
|
hasAccount: observable,
|
||||||
|
hasFirstPartySubscription: observable,
|
||||||
|
|
||||||
userSubscriptionName: computed,
|
userSubscriptionName: computed,
|
||||||
userSubscriptionExpirationDate: computed,
|
userSubscriptionExpirationDate: computed,
|
||||||
@@ -64,11 +67,20 @@ export class SubscriptionController extends AbstractViewController {
|
|||||||
this.reloadSubscriptionInvitations().catch(console.error)
|
this.reloadSubscriptionInvitations().catch(console.error)
|
||||||
}
|
}
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
this.hasFirstPartySubscription = application.features.hasFirstPartySubscription()
|
||||||
this.hasAccount = application.hasAccount()
|
this.hasAccount = application.hasAccount()
|
||||||
})
|
})
|
||||||
}, ApplicationEvent.Launched),
|
}, ApplicationEvent.Launched),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
this.disposers.push(
|
||||||
|
application.addEventObserver(async () => {
|
||||||
|
runInAction(() => {
|
||||||
|
this.hasFirstPartySubscription = application.features.hasFirstPartySubscription()
|
||||||
|
})
|
||||||
|
}, ApplicationEvent.LocalDataLoaded),
|
||||||
|
)
|
||||||
|
|
||||||
this.disposers.push(
|
this.disposers.push(
|
||||||
application.addEventObserver(async () => {
|
application.addEventObserver(async () => {
|
||||||
this.getSubscriptionInfo().catch(console.error)
|
this.getSubscriptionInfo().catch(console.error)
|
||||||
@@ -83,6 +95,9 @@ export class SubscriptionController extends AbstractViewController {
|
|||||||
application.addEventObserver(async () => {
|
application.addEventObserver(async () => {
|
||||||
this.getSubscriptionInfo().catch(console.error)
|
this.getSubscriptionInfo().catch(console.error)
|
||||||
this.reloadSubscriptionInvitations().catch(console.error)
|
this.reloadSubscriptionInvitations().catch(console.error)
|
||||||
|
runInAction(() => {
|
||||||
|
this.hasFirstPartySubscription = application.features.hasFirstPartySubscription()
|
||||||
|
})
|
||||||
}, ApplicationEvent.UserRolesChanged),
|
}, ApplicationEvent.UserRolesChanged),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -90,20 +105,20 @@ export class SubscriptionController extends AbstractViewController {
|
|||||||
get userSubscriptionName(): string {
|
get userSubscriptionName(): string {
|
||||||
if (
|
if (
|
||||||
this.availableSubscriptions &&
|
this.availableSubscriptions &&
|
||||||
this.userSubscription &&
|
this.onlineSubscription &&
|
||||||
this.availableSubscriptions[this.userSubscription.planName]
|
this.availableSubscriptions[this.onlineSubscription.planName]
|
||||||
) {
|
) {
|
||||||
return this.availableSubscriptions[this.userSubscription.planName].name
|
return this.availableSubscriptions[this.onlineSubscription.planName].name
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
get userSubscriptionExpirationDate(): Date | undefined {
|
get userSubscriptionExpirationDate(): Date | undefined {
|
||||||
if (!this.userSubscription) {
|
if (!this.onlineSubscription) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Date(convertTimestampToMilliseconds(this.userSubscription.endsAt))
|
return new Date(convertTimestampToMilliseconds(this.onlineSubscription.endsAt))
|
||||||
}
|
}
|
||||||
|
|
||||||
get isUserSubscriptionExpired(): boolean {
|
get isUserSubscriptionExpired(): boolean {
|
||||||
@@ -115,11 +130,11 @@ export class SubscriptionController extends AbstractViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get isUserSubscriptionCanceled(): boolean {
|
get isUserSubscriptionCanceled(): boolean {
|
||||||
return Boolean(this.userSubscription?.cancelled)
|
return Boolean(this.onlineSubscription?.cancelled)
|
||||||
}
|
}
|
||||||
|
|
||||||
hasValidSubscription(): boolean {
|
hasValidSubscription(): boolean {
|
||||||
return this.userSubscription != undefined && !this.isUserSubscriptionExpired && !this.isUserSubscriptionCanceled
|
return this.onlineSubscription != undefined && !this.isUserSubscriptionExpired && !this.isUserSubscriptionCanceled
|
||||||
}
|
}
|
||||||
|
|
||||||
get usedInvitationsCount(): number {
|
get usedInvitationsCount(): number {
|
||||||
@@ -139,7 +154,7 @@ export class SubscriptionController extends AbstractViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public setUserSubscription(subscription: Subscription): void {
|
public setUserSubscription(subscription: Subscription): void {
|
||||||
this.userSubscription = subscription
|
this.onlineSubscription = subscription
|
||||||
}
|
}
|
||||||
|
|
||||||
public setAvailableSubscriptions(subscriptions: AvailableSubscriptions): void {
|
public setAvailableSubscriptions(subscriptions: AvailableSubscriptions): void {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
ComponentArea,
|
ComponentArea,
|
||||||
FeatureDescription,
|
FeatureDescription,
|
||||||
GetFeatures,
|
GetFeatures,
|
||||||
|
FindNativeFeature,
|
||||||
NoteType,
|
NoteType,
|
||||||
FeatureIdentifier,
|
FeatureIdentifier,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
@@ -149,8 +150,7 @@ const createBaselineMap = (application: WebApplication): NoteTypeToEditorRowsMap
|
|||||||
isEntitled: application.features.getFeatureStatus(FeatureIdentifier.SuperEditor) === FeatureStatus.Entitled,
|
isEntitled: application.features.getFeatureStatus(FeatureIdentifier.SuperEditor) === FeatureStatus.Entitled,
|
||||||
noteType: NoteType.Super,
|
noteType: NoteType.Super,
|
||||||
isLabs: true,
|
isLabs: true,
|
||||||
description:
|
description: FindNativeFeature(FeatureIdentifier.SuperEditor)?.description,
|
||||||
'A new way to edit notes. Type / to bring up the block selection menu, or @ to embed images or link other tags and notes. Type - then space to start a list, or [] then space to start a checklist. Drag and drop an image or file to embed it in your note.',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[NoteType.RichText]: [],
|
[NoteType.RichText]: [],
|
||||||
|
|||||||
Reference in New Issue
Block a user