refactor: native feature management (#2350)

This commit is contained in:
Mo
2023-07-12 12:56:08 -05:00
committed by GitHub
parent 49f7581cd8
commit 078ef3772c
223 changed files with 3996 additions and 3438 deletions

View File

@@ -27,7 +27,6 @@
},
"devDependencies": {
"@types/jest": "^29.2.3",
"@types/lodash": "^4.14.189",
"@typescript-eslint/eslint-plugin": "*",
"eslint": "^8.27.0",
"eslint-plugin-prettier": "*",

View File

@@ -4,4 +4,6 @@ export enum SubscriptionApiOperations {
ListingInvites,
AcceptingInvite,
ConfirmAppleIAP,
GetSubscription,
GetAvailableSubscriptions,
}

View File

@@ -8,11 +8,17 @@ import { SubscriptionInviteAcceptResponseBody } from '../../Response/Subscriptio
import { SubscriptionInviteCancelResponseBody } from '../../Response/Subscription/SubscriptionInviteCancelResponseBody'
import { SubscriptionInviteListResponseBody } from '../../Response/Subscription/SubscriptionInviteListResponseBody'
import { SubscriptionInviteResponseBody } from '../../Response/Subscription/SubscriptionInviteResponseBody'
import { HttpResponse, ApiEndpointParam } from '@standardnotes/responses'
import {
HttpResponse,
ApiEndpointParam,
GetSubscriptionResponse,
GetAvailableSubscriptionsResponse,
} from '@standardnotes/responses'
import { SubscriptionApiServiceInterface } from './SubscriptionApiServiceInterface'
import { SubscriptionApiOperations } from './SubscriptionApiOperations'
import { AppleIAPConfirmRequestParams } from '../../Request'
import { GetUserSubscriptionRequestParams } from '../../Request/Subscription/GetUserSubscriptionRequestParams'
export class SubscriptionApiService implements SubscriptionApiServiceInterface {
private operationsInProgress: Map<SubscriptionApiOperations, boolean>
@@ -118,4 +124,36 @@ export class SubscriptionApiService implements SubscriptionApiServiceInterface {
this.operationsInProgress.set(SubscriptionApiOperations.ConfirmAppleIAP, false)
}
}
async getUserSubscription(params: GetUserSubscriptionRequestParams): Promise<HttpResponse<GetSubscriptionResponse>> {
if (this.operationsInProgress.get(SubscriptionApiOperations.GetSubscription)) {
throw new ApiCallError(ErrorMessage.GenericInProgress)
}
this.operationsInProgress.set(SubscriptionApiOperations.GetSubscription, true)
try {
const response = await this.subscriptionServer.getUserSubscription(params)
return response
} finally {
this.operationsInProgress.set(SubscriptionApiOperations.GetSubscription, false)
}
}
async getAvailableSubscriptions(): Promise<HttpResponse<GetAvailableSubscriptionsResponse>> {
if (this.operationsInProgress.get(SubscriptionApiOperations.GetAvailableSubscriptions)) {
throw new ApiCallError(ErrorMessage.GenericInProgress)
}
this.operationsInProgress.set(SubscriptionApiOperations.GetAvailableSubscriptions, true)
try {
const response = await this.subscriptionServer.getAvailableSubscriptions()
return response
} finally {
this.operationsInProgress.set(SubscriptionApiOperations.GetAvailableSubscriptions, false)
}
}
}

View File

@@ -4,7 +4,8 @@ import { SubscriptionInviteAcceptResponseBody } from '../../Response/Subscriptio
import { SubscriptionInviteCancelResponseBody } from '../../Response/Subscription/SubscriptionInviteCancelResponseBody'
import { SubscriptionInviteListResponseBody } from '../../Response/Subscription/SubscriptionInviteListResponseBody'
import { SubscriptionInviteResponseBody } from '../../Response/Subscription/SubscriptionInviteResponseBody'
import { HttpResponse } from '@standardnotes/responses'
import { GetAvailableSubscriptionsResponse, GetSubscriptionResponse, HttpResponse } from '@standardnotes/responses'
import { GetUserSubscriptionRequestParams } from '../../Request/Subscription/GetUserSubscriptionRequestParams'
export interface SubscriptionApiServiceInterface {
invite(inviteeEmail: string): Promise<HttpResponse<SubscriptionInviteResponseBody>>
@@ -12,4 +13,6 @@ export interface SubscriptionApiServiceInterface {
cancelInvite(inviteUuid: string): Promise<HttpResponse<SubscriptionInviteCancelResponseBody>>
acceptInvite(inviteUuid: string): Promise<HttpResponse<SubscriptionInviteAcceptResponseBody>>
confirmAppleIAP(params: AppleIAPConfirmRequestParams): Promise<HttpResponse<AppleIAPConfirmResponseBody>>
getUserSubscription(params: GetUserSubscriptionRequestParams): Promise<HttpResponse<GetSubscriptionResponse>>
getAvailableSubscriptions(): Promise<HttpResponse<GetAvailableSubscriptionsResponse>>
}

View File

@@ -0,0 +1,3 @@
export type GetUserSubscriptionRequestParams = {
userUuid: string
}

View File

@@ -6,13 +6,23 @@ const SharingPaths = {
listInvites: '/v1/subscription-invites',
}
const UserSubscriptionPaths = {
subscription: (userUuid: string) => `/v1/users/${userUuid}/subscription`,
}
const ApplePaths = {
confirmAppleIAP: '/v1/subscriptions/apple_iap_confirm',
}
const UnauthenticatedSubscriptionsPaths = {
availableSubscriptions: '/v2/subscriptions',
}
export const Paths = {
v1: {
...SharingPaths,
...ApplePaths,
...UserSubscriptionPaths,
...UnauthenticatedSubscriptionsPaths,
},
}

View File

@@ -11,10 +11,11 @@ import { SubscriptionInviteCancelResponseBody } from '../../Response/Subscriptio
import { SubscriptionInviteDeclineResponseBody } from '../../Response/Subscription/SubscriptionInviteDeclineResponseBody'
import { SubscriptionInviteListResponseBody } from '../../Response/Subscription/SubscriptionInviteListResponseBody'
import { SubscriptionInviteResponseBody } from '../../Response/Subscription/SubscriptionInviteResponseBody'
import { HttpResponse } from '@standardnotes/responses'
import { GetAvailableSubscriptionsResponse, GetSubscriptionResponse, HttpResponse } from '@standardnotes/responses'
import { Paths } from './Paths'
import { SubscriptionServerInterface } from './SubscriptionServerInterface'
import { GetUserSubscriptionRequestParams } from '../../Request/Subscription/GetUserSubscriptionRequestParams'
export class SubscriptionServer implements SubscriptionServerInterface {
constructor(private httpService: HttpServiceInterface) {}
@@ -50,4 +51,12 @@ export class SubscriptionServer implements SubscriptionServerInterface {
async confirmAppleIAP(params: AppleIAPConfirmRequestParams): Promise<HttpResponse<AppleIAPConfirmResponseBody>> {
return this.httpService.post(Paths.v1.confirmAppleIAP, params)
}
async getUserSubscription(params: GetUserSubscriptionRequestParams): Promise<HttpResponse<GetSubscriptionResponse>> {
return this.httpService.get(Paths.v1.subscription(params.userUuid), params)
}
async getAvailableSubscriptions(): Promise<HttpResponse<GetAvailableSubscriptionsResponse>> {
return this.httpService.get(Paths.v1.availableSubscriptions)
}
}

View File

@@ -11,7 +11,8 @@ import { SubscriptionInviteCancelResponseBody } from '../../Response/Subscriptio
import { SubscriptionInviteDeclineResponseBody } from '../../Response/Subscription/SubscriptionInviteDeclineResponseBody'
import { SubscriptionInviteListResponseBody } from '../../Response/Subscription/SubscriptionInviteListResponseBody'
import { SubscriptionInviteResponseBody } from '../../Response/Subscription/SubscriptionInviteResponseBody'
import { HttpResponse } from '@standardnotes/responses'
import { GetAvailableSubscriptionsResponse, GetSubscriptionResponse, HttpResponse } from '@standardnotes/responses'
import { GetUserSubscriptionRequestParams } from '../../Request/Subscription/GetUserSubscriptionRequestParams'
export interface SubscriptionServerInterface {
invite(params: SubscriptionInviteRequestParams): Promise<HttpResponse<SubscriptionInviteResponseBody>>
@@ -25,5 +26,9 @@ export interface SubscriptionServerInterface {
params: SubscriptionInviteCancelRequestParams,
): Promise<HttpResponse<SubscriptionInviteCancelResponseBody>>
listInvites(params: SubscriptionInviteListRequestParams): Promise<HttpResponse<SubscriptionInviteListResponseBody>>
confirmAppleIAP(params: AppleIAPConfirmRequestParams): Promise<HttpResponse<AppleIAPConfirmResponseBody>>
getUserSubscription(params: GetUserSubscriptionRequestParams): Promise<HttpResponse<GetSubscriptionResponse>>
getAvailableSubscriptions(): Promise<HttpResponse<GetAvailableSubscriptionsResponse>>
}

View File

@@ -10,11 +10,14 @@ import { FileErrorCodes } from './File/FileErrorCodes'
const Protocol = 'http'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function logError(...message: any) {
console.error('extServer:', ...message)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function log(...message: any) {
// eslint-disable-next-line no-console
console.log('extServer:', ...message)
}
@@ -71,8 +74,8 @@ async function handleRequest(request: IncomingMessage, response: ServerResponse)
response.writeHead(200)
response.end(data)
} catch (error: any) {
onRequestError(error, response)
} catch (error) {
onRequestError(error as Error, response)
}
}

View File

@@ -1,4 +1,6 @@
import { EditorFeatureDescription } from '../Feature/EditorFeatureDescription'
import { FindNativeFeature } from '../Feature/Features'
import { IframeComponentFeatureDescription } from '../Feature/IframeComponentFeatureDescription'
import { FeatureIdentifier } from './../Feature/FeatureIdentifier'
import { EditorIdentifier } from './EditorIdentifier'
@@ -15,13 +17,9 @@ export enum NoteType {
}
export function noteTypeForEditorIdentifier(identifier: EditorIdentifier): NoteType {
if (identifier === FeatureIdentifier.PlainEditor) {
return NoteType.Plain
} else if (identifier === FeatureIdentifier.SuperEditor) {
return NoteType.Super
}
const feature = FindNativeFeature(identifier as FeatureIdentifier)
const feature = FindNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>(
identifier as FeatureIdentifier,
)
if (feature && feature.note_type) {
return feature.note_type
}

View File

@@ -0,0 +1,14 @@
import { EditorFeatureDescription } from './EditorFeatureDescription'
import { ThemeFeatureDescription } from './ThemeFeatureDescription'
import { ComponentFeatureDescription } from './ComponentFeatureDescription'
import { IframeComponentFeatureDescription } from './IframeComponentFeatureDescription'
import { ClientFeatureDescription } from './ClientFeatureDescription'
import { ServerFeatureDescription } from './ServerFeatureDescription'
export type AnyFeatureDescription =
| ComponentFeatureDescription
| EditorFeatureDescription
| ThemeFeatureDescription
| IframeComponentFeatureDescription
| ClientFeatureDescription
| ServerFeatureDescription

View File

@@ -0,0 +1,24 @@
import { PermissionName } from '../Permission/PermissionName'
import { FeatureIdentifier } from './FeatureIdentifier'
import { ComponentFlag } from '../Component/ComponentFlag'
import { RoleFields } from './RoleFields'
export type BaseFeatureDescription = RoleFields & {
deletion_warning?: string
deprecated?: boolean
deprecation_message?: string
description?: string
expires_at?: number
/** Whether the client controls availability of this feature (such as the dark theme) */
clientControlled?: boolean
flags?: ComponentFlag[]
identifier: FeatureIdentifier
marketing_url?: string
name: string
no_expire?: boolean
no_mobile?: boolean
thumbnail_url?: string
permission_name: PermissionName
}

View File

@@ -0,0 +1,11 @@
import { PermissionName } from '../Permission/PermissionName'
import { FeatureIdentifier } from './FeatureIdentifier'
import { RoleFields } from './RoleFields'
export type ClientFeatureDescription = RoleFields & {
identifier: FeatureIdentifier
permission_name: PermissionName
description: string
name: string
deprecated?: boolean
}

View File

@@ -0,0 +1,9 @@
import { ComponentArea } from '../Component/ComponentArea'
import { BaseFeatureDescription } from './BaseFeatureDescription'
export type ComponentFeatureDescription = BaseFeatureDescription & {
/** The relative path of the index.html file or the main css file if theme, within the component folder itself */
index_path: string
content_type: string
area: ComponentArea
}

View File

@@ -0,0 +1,10 @@
import { NoteType } from '../Component/NoteType'
import { BaseFeatureDescription } from './BaseFeatureDescription'
export type EditorFeatureDescription = BaseFeatureDescription & {
file_type: 'txt' | 'html' | 'md' | 'json'
/** Whether an editor is interchangable with another editor that has the same file_type */
interchangeable: boolean
note_type: NoteType
spellcheckControl: boolean
}

View File

@@ -1,81 +0,0 @@
import { ComponentPermission } from '../Component/ComponentPermission'
import { ComponentArea } from '../Component/ComponentArea'
import { PermissionName } from '../Permission/PermissionName'
import { FeatureIdentifier } from './FeatureIdentifier'
import { ComponentFlag } from '../Component/ComponentFlag'
import { NoteType } from '../Component/NoteType'
import { ThemeDockIcon } from '../Component/ThemeDockIcon'
type RoleFields = {
/** Server populated */
role_name?: string
/** Statically populated. Non-influencing; used as a reference by other static consumers (such as email service) */
availableInRoles: string[]
}
export type BaseFeatureDescription = RoleFields & {
deletion_warning?: string
deprecated?: boolean
deprecation_message?: string
description?: string
expires_at?: number
/** Whether the client controls availability of this feature (such as the dark theme) */
clientControlled?: boolean
flags?: ComponentFlag[]
identifier: FeatureIdentifier
marketing_url?: string
name?: string
no_expire?: boolean
no_mobile?: boolean
thumbnail_url?: string
permission_name: PermissionName
}
export type ServerFeatureDescription = RoleFields & {
name?: string
identifier: FeatureIdentifier
permission_name: PermissionName
}
export type ClientFeatureDescription = RoleFields & {
identifier: FeatureIdentifier
permission_name: PermissionName
description: string
name: string
}
export type ComponentFeatureDescription = BaseFeatureDescription & {
/** The relative path of the index.html file or the main css file if theme, within the component folder itself */
index_path: string
content_type: string
area: ComponentArea
}
export type ThirdPartyFeatureDescription = ComponentFeatureDescription & {
url: string
}
export type IframeComponentFeatureDescription = ComponentFeatureDescription & {
component_permissions: ComponentPermission[]
}
export type EditorFeatureDescription = IframeComponentFeatureDescription & {
file_type: 'txt' | 'html' | 'md' | 'json'
/** Whether an editor is interchangable with another editor that has the same file_type */
interchangeable: boolean
note_type: NoteType
spellcheckControl?: boolean
}
export type ThemeFeatureDescription = ComponentFeatureDescription & {
/** Some themes can be layered on top of other themes */
layerable?: boolean
dock_icon?: ThemeDockIcon
isDark?: boolean
}
export type FeatureDescription = BaseFeatureDescription &
Partial<ComponentFeatureDescription & EditorFeatureDescription & ThemeFeatureDescription>

View File

@@ -1,14 +1,51 @@
import { FeatureDescription } from './FeatureDescription'
import { AnyFeatureDescription } from './AnyFeatureDescription'
import { ThemeFeatureDescription } from './ThemeFeatureDescription'
import { EditorFeatureDescription } from './EditorFeatureDescription'
import { FeatureIdentifier } from './FeatureIdentifier'
import { serverFeatures } from '../Lists/ServerFeatures'
import { clientFeatures } from '../Lists/ClientFeatures'
import { GetDeprecatedFeatures } from '../Lists/DeprecatedFeatures'
import { experimentalFeatures } from '../Lists/ExperimentalFeatures'
import { IframeEditors } from '../Lists/IframeEditors'
import { themes } from '../Lists/Themes'
import { nativeEditors } from '../Lists/NativeEditors'
export function GetFeatures(): FeatureDescription[] {
return [...serverFeatures(), ...clientFeatures(), ...experimentalFeatures(), ...GetDeprecatedFeatures()]
export function GetFeatures(): AnyFeatureDescription[] {
return [
...serverFeatures(),
...clientFeatures(),
...themes(),
...nativeEditors(),
...IframeEditors(),
...experimentalFeatures(),
...GetDeprecatedFeatures(),
]
}
export function FindNativeFeature(identifier: FeatureIdentifier): FeatureDescription | undefined {
return GetFeatures().find((f) => f.identifier === identifier)
export function FindNativeFeature<T extends AnyFeatureDescription>(identifier: FeatureIdentifier): T | undefined {
return GetFeatures().find((f) => f.identifier === identifier) as T
}
export function FindNativeTheme(identifier: FeatureIdentifier): ThemeFeatureDescription | undefined {
return themes().find((t) => t.identifier === identifier)
}
export function GetIframeAndNativeEditors(): EditorFeatureDescription[] {
return [...IframeEditors(), ...nativeEditors()]
}
export function GetSuperNoteFeature(): EditorFeatureDescription {
return FindNativeFeature(FeatureIdentifier.SuperEditor) as EditorFeatureDescription
}
export function GetPlainNoteFeature(): EditorFeatureDescription {
return FindNativeFeature(FeatureIdentifier.PlainEditor) as EditorFeatureDescription
}
export function GetNativeThemes(): ThemeFeatureDescription[] {
return themes()
}
export function GetDarkThemeFeature(): ThemeFeatureDescription {
return themes().find((t) => t.identifier === FeatureIdentifier.DarkTheme) as ThemeFeatureDescription
}

View File

@@ -0,0 +1,7 @@
import { ComponentPermission } from '../Component/ComponentPermission'
import { ComponentFeatureDescription } from './ComponentFeatureDescription'
import { EditorFeatureDescription } from './EditorFeatureDescription'
export type IframeComponentFeatureDescription = (EditorFeatureDescription & ComponentFeatureDescription) & {
component_permissions: ComponentPermission[]
}

View File

@@ -0,0 +1,7 @@
export type RoleFields = {
/** Server populated */
role_name?: string
/** Statically populated. Non-influencing; used as a reference by other static consumers (such as email service) */
availableInRoles: string[]
}

View File

@@ -0,0 +1,11 @@
import { PermissionName } from '../Permission/PermissionName'
import { FeatureIdentifier } from './FeatureIdentifier'
import { RoleFields } from './RoleFields'
export type ServerFeatureDescription = RoleFields & {
name: string
description?: string
identifier: FeatureIdentifier
permission_name: PermissionName
deprecated?: boolean
}

View File

@@ -0,0 +1,9 @@
import { ThemeDockIcon } from '../Component/ThemeDockIcon'
import { ComponentFeatureDescription } from './ComponentFeatureDescription'
export type ThemeFeatureDescription = ComponentFeatureDescription & {
/** Some themes can be layered on top of other themes */
layerable?: boolean
dock_icon?: ThemeDockIcon
isDark?: boolean
}

View File

@@ -0,0 +1,5 @@
import { ComponentFeatureDescription } from './ComponentFeatureDescription'
export type ThirdPartyFeatureDescription = ComponentFeatureDescription & {
url: string
}

View File

@@ -0,0 +1,28 @@
import { ContentType } from '@standardnotes/domain-core'
import { AnyFeatureDescription } from './AnyFeatureDescription'
import { ThemeFeatureDescription } from './ThemeFeatureDescription'
import { EditorFeatureDescription } from './EditorFeatureDescription'
import { IframeComponentFeatureDescription } from './IframeComponentFeatureDescription'
import { ComponentFeatureDescription } from './ComponentFeatureDescription'
import { ComponentArea } from '../Component/ComponentArea'
export function isThemeFeatureDescription(feature: AnyFeatureDescription): feature is ThemeFeatureDescription {
return 'content_type' in feature && feature.content_type === ContentType.TYPES.Theme
}
export function isIframeComponentFeatureDescription(
feature: AnyFeatureDescription,
): feature is IframeComponentFeatureDescription {
return (
'content_type' in feature &&
feature.content_type === ContentType.TYPES.Component &&
[ComponentArea.Editor, ComponentArea.EditorStack].includes(feature.area)
)
}
export function isEditorFeatureDescription(feature: AnyFeatureDescription): feature is EditorFeatureDescription {
return (
(feature as EditorFeatureDescription).note_type != undefined ||
(feature as ComponentFeatureDescription).area === ComponentArea.Editor
)
}

View File

@@ -0,0 +1,10 @@
import { ComponentFeatureDescription } from './ComponentFeatureDescription'
import { EditorFeatureDescription } from './EditorFeatureDescription'
import { IframeComponentFeatureDescription } from './IframeComponentFeatureDescription'
import { ThemeFeatureDescription } from './ThemeFeatureDescription'
export type UIFeatureDescriptionTypes =
| IframeComponentFeatureDescription
| ThemeFeatureDescription
| EditorFeatureDescription
| ComponentFeatureDescription

View File

@@ -1,14 +1,10 @@
import { FeatureDescription } from '../Feature/FeatureDescription'
import { PermissionName } from '../Permission/PermissionName'
import { FeatureIdentifier } from '../Feature/FeatureIdentifier'
import { RoleName } from '@standardnotes/domain-core'
import { themes } from './Themes'
import { editors } from './Editors'
import { ClientFeatureDescription } from '../Feature/ClientFeatureDescription'
export function clientFeatures(): FeatureDescription[] {
export function clientFeatures(): ClientFeatureDescription[] {
return [
...themes(),
...editors(),
{
name: 'Tag Nesting',
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
@@ -16,14 +12,7 @@ export function clientFeatures(): FeatureDescription[] {
permission_name: PermissionName.TagNesting,
description: 'Organize your tags into folders.',
},
{
name: 'Super Notes',
identifier: FeatureIdentifier.SuperEditor,
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
permission_name: PermissionName.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. Cmd/Ctrl + F to bring up search and replace.',
},
{
name: 'Smart Filters',
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],

View File

@@ -1,18 +1,16 @@
import { AnyFeatureDescription } from '../Feature/AnyFeatureDescription'
import { EditorFeatureDescription } from '../Feature/EditorFeatureDescription'
import { IframeComponentFeatureDescription } from '../Feature/IframeComponentFeatureDescription'
import { ContentType, RoleName } from '@standardnotes/domain-core'
import {
EditorFeatureDescription,
IframeComponentFeatureDescription,
FeatureDescription,
} from '../Feature/FeatureDescription'
import { PermissionName } from '../Permission/PermissionName'
import { FeatureIdentifier } from '../Feature/FeatureIdentifier'
import { NoteType } from '../Component/NoteType'
import { FillEditorComponentDefaults } from './Utilities/FillEditorComponentDefaults'
import { FillIframeEditorDefaults } from './Utilities/FillEditorComponentDefaults'
import { ComponentAction } from '../Component/ComponentAction'
import { ComponentArea } from '../Component/ComponentArea'
export function GetDeprecatedFeatures(): FeatureDescription[] {
const bold: EditorFeatureDescription = FillEditorComponentDefaults({
export function GetDeprecatedFeatures(): AnyFeatureDescription[] {
const bold: EditorFeatureDescription = FillIframeEditorDefaults({
name: 'Alternative Rich Text',
identifier: FeatureIdentifier.DeprecatedBoldEditor,
note_type: NoteType.RichText,
@@ -39,7 +37,7 @@ export function GetDeprecatedFeatures(): FeatureDescription[] {
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
})
const markdownBasic: EditorFeatureDescription = FillEditorComponentDefaults({
const markdownBasic: EditorFeatureDescription = FillIframeEditorDefaults({
name: 'Basic Markdown',
identifier: FeatureIdentifier.DeprecatedMarkdownBasicEditor,
note_type: NoteType.Markdown,
@@ -52,7 +50,7 @@ export function GetDeprecatedFeatures(): FeatureDescription[] {
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
})
const markdownAlt: EditorFeatureDescription = FillEditorComponentDefaults({
const markdownAlt: EditorFeatureDescription = FillIframeEditorDefaults({
name: 'Markdown Alternative',
identifier: FeatureIdentifier.DeprecatedMarkdownVisualEditor,
note_type: NoteType.Markdown,
@@ -66,7 +64,7 @@ export function GetDeprecatedFeatures(): FeatureDescription[] {
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
})
const markdownMinimist: EditorFeatureDescription = FillEditorComponentDefaults({
const markdownMinimist: EditorFeatureDescription = FillIframeEditorDefaults({
name: 'Minimal Markdown',
identifier: FeatureIdentifier.DeprecatedMarkdownMinimistEditor,
note_type: NoteType.Markdown,
@@ -80,7 +78,7 @@ export function GetDeprecatedFeatures(): FeatureDescription[] {
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
})
const markdownMath: EditorFeatureDescription = FillEditorComponentDefaults({
const markdownMath: EditorFeatureDescription = FillIframeEditorDefaults({
name: 'Markdown with Math',
identifier: FeatureIdentifier.DeprecatedMarkdownMathEditor,
spellcheckControl: true,
@@ -94,7 +92,7 @@ export function GetDeprecatedFeatures(): FeatureDescription[] {
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
})
const filesafe: IframeComponentFeatureDescription = FillEditorComponentDefaults({
const filesafe: IframeComponentFeatureDescription = FillIframeEditorDefaults({
name: 'FileSafe',
identifier: FeatureIdentifier.DeprecatedFileSafe,
component_permissions: [

View File

@@ -1,5 +1,5 @@
import { FeatureDescription } from '../Feature/FeatureDescription'
import { AnyFeatureDescription } from '../Feature/AnyFeatureDescription'
export function experimentalFeatures(): FeatureDescription[] {
export function experimentalFeatures(): AnyFeatureDescription[] {
return []
}

View File

@@ -1,12 +1,12 @@
import { EditorFeatureDescription } from '../Feature/FeatureDescription'
import { PermissionName } from '../Permission/PermissionName'
import { FeatureIdentifier } from '../Feature/FeatureIdentifier'
import { NoteType } from '../Component/NoteType'
import { FillEditorComponentDefaults } from './Utilities/FillEditorComponentDefaults'
import { FillIframeEditorDefaults } from './Utilities/FillEditorComponentDefaults'
import { RoleName } from '@standardnotes/domain-core'
import { IframeComponentFeatureDescription } from '../Feature/IframeComponentFeatureDescription'
export function editors(): EditorFeatureDescription[] {
const code: EditorFeatureDescription = FillEditorComponentDefaults({
export function IframeEditors(): IframeComponentFeatureDescription[] {
const code = FillIframeEditorDefaults({
name: 'Code',
spellcheckControl: true,
identifier: FeatureIdentifier.CodeEditor,
@@ -22,7 +22,7 @@ export function editors(): EditorFeatureDescription[] {
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
})
const plus: EditorFeatureDescription = FillEditorComponentDefaults({
const plus = FillIframeEditorDefaults({
name: 'Rich Text',
note_type: NoteType.RichText,
file_type: 'html',
@@ -35,7 +35,7 @@ export function editors(): EditorFeatureDescription[] {
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
})
const markdown: EditorFeatureDescription = FillEditorComponentDefaults({
const markdown = FillIframeEditorDefaults({
name: 'Markdown',
identifier: FeatureIdentifier.MarkdownProEditor,
note_type: NoteType.Markdown,
@@ -48,7 +48,7 @@ export function editors(): EditorFeatureDescription[] {
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
})
const task: EditorFeatureDescription = FillEditorComponentDefaults({
const task = FillIframeEditorDefaults({
name: 'Checklist',
identifier: FeatureIdentifier.TaskEditor,
note_type: NoteType.Task,
@@ -62,7 +62,7 @@ export function editors(): EditorFeatureDescription[] {
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
})
const tokenvault: EditorFeatureDescription = FillEditorComponentDefaults({
const tokenvault = FillIframeEditorDefaults({
name: 'Authenticator',
note_type: NoteType.Authentication,
file_type: 'json',
@@ -75,7 +75,7 @@ export function editors(): EditorFeatureDescription[] {
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
})
const spreadsheets: EditorFeatureDescription = FillEditorComponentDefaults({
const spreadsheets = FillIframeEditorDefaults({
name: 'Spreadsheet',
identifier: FeatureIdentifier.SheetsEditor,
note_type: NoteType.Spreadsheet,

View File

@@ -0,0 +1,32 @@
import { RoleName } from '@standardnotes/domain-core'
import { NoteType } from '../Component/NoteType'
import { EditorFeatureDescription } from '../Feature/EditorFeatureDescription'
import { FeatureIdentifier } from '../Feature/FeatureIdentifier'
import { PermissionName } from '../Permission/PermissionName'
export function nativeEditors(): EditorFeatureDescription[] {
return [
{
name: 'Super',
note_type: NoteType.Super,
identifier: FeatureIdentifier.SuperEditor,
spellcheckControl: true,
file_type: 'json',
interchangeable: false,
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
permission_name: PermissionName.SuperEditor,
description:
'The best 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.',
},
{
name: 'Plain Text',
note_type: NoteType.Plain,
spellcheckControl: true,
file_type: 'txt',
interchangeable: true,
identifier: FeatureIdentifier.PlainEditor,
availableInRoles: [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
permission_name: PermissionName.PlainEditor,
},
]
}

View File

@@ -1,4 +1,4 @@
import { ServerFeatureDescription } from '../Feature/FeatureDescription'
import { ServerFeatureDescription } from '../Feature/ServerFeatureDescription'
import { PermissionName } from '../Permission/PermissionName'
import { FeatureIdentifier } from '../Feature/FeatureIdentifier'
import { RoleName } from '@standardnotes/domain-core'
@@ -42,16 +42,19 @@ export function serverFeatures(): ServerFeatureDescription[] {
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
},
{
name: 'Files maximum storage tier',
identifier: FeatureIdentifier.FilesMaximumStorageTier,
permission_name: PermissionName.FilesMaximumStorageTier,
availableInRoles: [RoleName.NAMES.ProUser],
},
{
name: 'Files low storage tier',
identifier: FeatureIdentifier.FilesLowStorageTier,
permission_name: PermissionName.FilesLowStorageTier,
availableInRoles: [RoleName.NAMES.PlusUser],
},
{
name: 'Files medium storage tier',
identifier: FeatureIdentifier.SubscriptionSharing,
permission_name: PermissionName.SubscriptionSharing,
availableInRoles: [RoleName.NAMES.ProUser],

View File

@@ -1,4 +1,4 @@
import { ThemeFeatureDescription } from '../Feature/FeatureDescription'
import { ThemeFeatureDescription } from '../Feature/ThemeFeatureDescription'
import { PermissionName } from '../Permission/PermissionName'
import { FeatureIdentifier } from '../Feature/FeatureIdentifier'
import { FillThemeComponentDefaults } from './Utilities/FillThemeComponentDefaults'

View File

@@ -1,14 +1,15 @@
import { ContentType } from '@standardnotes/domain-core'
import { ComponentAction } from '../../Component/ComponentAction'
import { EditorFeatureDescription } from '../../Feature/FeatureDescription'
import { EditorFeatureDescription } from '../../Feature/EditorFeatureDescription'
import { IframeComponentFeatureDescription } from '../../Feature/IframeComponentFeatureDescription'
import { ComponentArea } from '../../Component/ComponentArea'
export type RequiredEditorFields = Pick<EditorFeatureDescription, 'availableInRoles'>
export function FillEditorComponentDefaults(
component: Partial<EditorFeatureDescription> & RequiredEditorFields,
): EditorFeatureDescription {
export function FillIframeEditorDefaults(
component: Partial<IframeComponentFeatureDescription> & RequiredEditorFields,
): IframeComponentFeatureDescription {
if (!component.index_path) {
component.index_path = 'dist/index.html'
}
@@ -31,5 +32,5 @@ export function FillEditorComponentDefaults(
component.interchangeable = true
}
return component as EditorFeatureDescription
return component as IframeComponentFeatureDescription
}

View File

@@ -1,6 +1,5 @@
import { ContentType } from '@standardnotes/domain-core'
import { ThemeFeatureDescription } from '../../Feature/FeatureDescription'
import { ThemeFeatureDescription } from '../../Feature/ThemeFeatureDescription'
import { ComponentArea } from '../../Component/ComponentArea'
type RequiredThemeFields = Pick<ThemeFeatureDescription, 'availableInRoles'>

View File

@@ -22,6 +22,7 @@ export enum PermissionName {
NoteHistory30Days = 'server:note-history-30-days',
NoteHistory365Days = 'server:note-history-365-days',
NoteHistoryUnlimited = 'server:note-history-unlimited',
PlainEditor = 'editor:plain',
PlusEditor = 'editor:plus',
SheetsEditor = 'editor:sheets',
SignInAlerts = 'server:sign-in-alerts',

View File

@@ -1,6 +1,17 @@
export * from './Feature/FeatureDescription'
export * from './Feature/AnyFeatureDescription'
export * from './Feature/FeatureIdentifier'
export * from './Feature/Features'
export * from './Feature/TypeGuards'
export * from './Feature/ThirdPartyFeatureDescription'
export * from './Feature/ClientFeatureDescription'
export * from './Feature/ServerFeatureDescription'
export * from './Feature/IframeComponentFeatureDescription'
export * from './Feature/ComponentFeatureDescription'
export * from './Feature/BaseFeatureDescription'
export * from './Feature/EditorFeatureDescription'
export * from './Feature/ThemeFeatureDescription'
export * from './Feature/UIFeatureDescription'
export * from './Permission/Permission'
export * from './Permission/PermissionName'

View File

@@ -637,7 +637,7 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/yoga"
SPEC CHECKSUMS:
boost: 57d2868c099736d80fcd648bf211b4431e51a558
boost: a7c83b31436843459a1961bfd74b96033dc77234
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
FBLazyVector: 60195509584153283780abdac5569feffb8f08cc
@@ -658,7 +658,7 @@ SPEC CHECKSUMS:
MMKV: 9c4663aa7ca255d478ff10f2f5cb7d17c1651ccd
MMKVCore: 89f5c8a66bba2dcd551779dea4d412eeec8ff5bb
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda
RCTRequired: bec48f07daf7bcdc2655a0cde84e07d24d2a9e2a
RCTTypeSafety: 171394eebacf71e1cfad79dbfae7ee8fc16ca80a
React: d7433ccb6a8c36e4cbed59a73c0700fc83c3e98a

View File

@@ -1,8 +1,8 @@
import { ComponentPermission } from '@standardnotes/features'
import { SNComponent } from '../../Syncable/Component'
import { ComponentInterface } from '../../Syncable/Component'
export type PermissionDialog = {
component: SNComponent
component: ComponentInterface
permissions: ComponentPermission[]
permissionsString: string
actionBlock: (approved: boolean) => void

View File

@@ -5,7 +5,7 @@ import { DecryptedItemInterface } from './DecryptedItem'
import { isDecryptedPayload, isDeletedPayload, isEncryptedPayload } from '../../Payload/Interfaces/TypeCheck'
export function isDecryptedItem(item: ItemInterface): item is DecryptedItemInterface {
return isDecryptedPayload(item.payload)
return 'payload' in item && isDecryptedPayload(item.payload)
}
export function isEncryptedItem(item: ItemInterface): item is EncryptedItemInterface {

View File

@@ -19,7 +19,7 @@ export class DecryptedItemMutator<
constructor(item: I, type: MutationType) {
super(item, type)
const mutableCopy = Copy(this.immutablePayload.content)
const mutableCopy = Copy<C>(this.immutablePayload.content)
this.mutableContent = mutableCopy
}

View File

@@ -58,7 +58,7 @@ describe('component model', () => {
package_info: {
note_type: NoteType.Authentication,
},
} as ComponentContent),
} as unknown as ComponentContent),
...PayloadTimestampDefaults(),
},
PayloadSource.Constructor,

View File

@@ -7,9 +7,11 @@ import {
ComponentPermission,
FindNativeFeature,
NoteType,
isEditorFeatureDescription,
} from '@standardnotes/features'
import { AppDataField } from '../../Abstract/Item/Types/AppDataField'
import { ComponentContent, ComponentInterface } from './ComponentContent'
import { ComponentContent } from './ComponentContent'
import { ComponentInterface } from './ComponentInterface'
import { ConflictStrategy } from '../../Abstract/Item/Types/ConflictStrategy'
import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem'
import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload'
@@ -19,12 +21,24 @@ import { Predicate } from '../../Runtime/Predicate/Predicate'
import { ItemInterface } from '../../Abstract/Item/Interfaces/ItemInterface'
import { DecryptedItemInterface } from './../../Abstract/Item/Interfaces/DecryptedItem'
import { ComponentPackageInfo } from './PackageInfo'
import { isDecryptedItem } from '../../Abstract/Item'
import { ContentType } from '@standardnotes/domain-core'
export const isComponent = (x: ItemInterface): x is SNComponent => x.content_type === ContentType.TYPES.Component
export function isComponent(x: ItemInterface): x is ComponentInterface {
if (!isDecryptedItem(x as DecryptedItemInterface)) {
return false
}
export const isComponentOrTheme = (x: ItemInterface): x is SNComponent =>
x.content_type === ContentType.TYPES.Component || x.content_type === ContentType.TYPES.Theme
return x.content_type === ContentType.TYPES.Component
}
export function isComponentOrTheme(x: ItemInterface): x is ComponentInterface {
if (!isDecryptedItem(x as DecryptedItemInterface)) {
return false
}
return x.content_type === ContentType.TYPES.Component || x.content_type === ContentType.TYPES.Theme
}
/**
* Components are mostly iframe based extensions that communicate with the SN parent
@@ -32,7 +46,7 @@ export const isComponentOrTheme = (x: ItemInterface): x is SNComponent =>
* only by its url.
*/
export class SNComponent extends DecryptedItem<ComponentContent> implements ComponentInterface {
public readonly componentData: Record<string, unknown>
public readonly legacyComponentData: Record<string, unknown>
/** Items that have requested a component to be disabled in its context */
public readonly disassociatedItemIds: string[]
/** Items that have requested a component to be enabled in its context */
@@ -46,14 +60,12 @@ export class SNComponent extends DecryptedItem<ComponentContent> implements Comp
public readonly area: ComponentArea
public readonly permissions: ComponentPermission[] = []
public readonly valid_until: Date
public readonly active: boolean
public readonly legacyActive: boolean
public readonly legacy_url?: string
public readonly isMobileDefault: boolean
constructor(payload: DecryptedPayloadInterface<ComponentContent>) {
super(payload)
/** Custom data that a component can store in itself */
this.componentData = this.payload.content.componentData || {}
if (payload.content.hosted_url && isValidUrl(payload.content.hosted_url)) {
this.hosted_url = payload.content.hosted_url
@@ -65,16 +77,16 @@ export class SNComponent extends DecryptedItem<ComponentContent> implements Comp
this.local_url = payload.content.local_url
this.valid_until = new Date(payload.content.valid_until || 0)
this.offlineOnly = payload.content.offlineOnly
this.offlineOnly = payload.content.offlineOnly ?? false
this.name = payload.content.name
this.area = payload.content.area
this.package_info = payload.content.package_info || {}
this.permissions = payload.content.permissions || []
this.active = payload.content.active
this.autoupdateDisabled = payload.content.autoupdateDisabled
this.autoupdateDisabled = payload.content.autoupdateDisabled ?? false
this.disassociatedItemIds = payload.content.disassociatedItemIds || []
this.associatedItemIds = payload.content.associatedItemIds || []
this.isMobileDefault = payload.content.isMobileDefault
this.isMobileDefault = payload.content.isMobileDefault ?? false
/**
* @legacy
* We don't want to set this.url directly, as we'd like to phase it out.
@@ -83,6 +95,10 @@ export class SNComponent extends DecryptedItem<ComponentContent> implements Comp
* hosted_url is the url replacement.
*/
this.legacy_url = !payload.content.hosted_url ? payload.content.url : undefined
this.legacyComponentData = this.payload.content.componentData || {}
this.legacyActive = payload.content.active ?? false
}
/** Do not duplicate components under most circumstances. Always keep original */
@@ -123,18 +139,6 @@ export class SNComponent extends DecryptedItem<ComponentContent> implements Comp
return this.getAppDomainValue(AppDataField.LastSize)
}
/**
* The key used to look up data that this component may have saved to an item.
* This data will be stored on the item using this key.
*/
public getClientDataKey(): string {
if (this.legacy_url) {
return this.legacy_url
} else {
return this.uuid
}
}
public hasValidHostedUrl(): boolean {
return (this.hosted_url || this.legacy_url) != undefined
}
@@ -180,7 +184,11 @@ export class SNComponent extends DecryptedItem<ComponentContent> implements Comp
}
public get noteType(): NoteType {
return this.package_info.note_type || NoteType.Unknown
if (isEditorFeatureDescription(this.package_info)) {
return this.package_info.note_type ?? NoteType.Unknown
}
return NoteType.Unknown
}
public get isDeprecated(): boolean {

View File

@@ -1,35 +1,40 @@
import { ComponentArea, ComponentPermission } from '@standardnotes/features'
import { ItemContent } from '../../Abstract/Content/ItemContent'
import { ComponentPackageInfo } from './PackageInfo'
import { ItemContent } from '../../Abstract/Content/ItemContent'
/* eslint-disable @typescript-eslint/no-explicit-any */
export interface ComponentInterface {
componentData: Record<string, any>
export type ComponentContentSpecialized = {
/** Items that have requested a component to be disabled in its context */
disassociatedItemIds: string[]
disassociatedItemIds?: string[]
/** Items that have requested a component to be enabled in its context */
associatedItemIds: string[]
associatedItemIds?: string[]
local_url?: string
hosted_url?: string
offlineOnly?: boolean
name: string
autoupdateDisabled?: boolean
package_info: ComponentPackageInfo
area: ComponentArea
permissions?: ComponentPermission[]
valid_until: Date | number
legacy_url?: string
isMobileDefault?: boolean
isDeprecated?: boolean
/** @deprecated */
active?: boolean
/** @deprecated */
url?: string
offlineOnly: boolean
name: string
autoupdateDisabled: boolean
package_info: ComponentPackageInfo
area: ComponentArea
permissions: ComponentPermission[]
valid_until: Date | number
active: boolean
legacy_url?: string
isMobileDefault: boolean
isDeprecated: boolean
isExplicitlyEnabledForItem(uuid: string): boolean
/**
* @deprecated
* Replaced with per-note component data stored in the note's ComponentDataDomain.
*/
componentData?: Record<string, unknown>
}
export type ComponentContent = ComponentInterface & ItemContent
export type ComponentContent = ItemContent & ComponentContentSpecialized

View File

@@ -0,0 +1,63 @@
import {
ComponentArea,
ComponentPermission,
FeatureIdentifier,
NoteType,
ThirdPartyFeatureDescription,
} from '@standardnotes/features'
import { ComponentPackageInfo } from './PackageInfo'
import { DecryptedItemInterface } from '../../Abstract/Item'
import { ComponentContent } from './ComponentContent'
export interface ComponentInterface extends DecryptedItemInterface<ComponentContent> {
/** Items that have requested a component to be disabled in its context */
disassociatedItemIds: string[]
/** Items that have requested a component to be enabled in its context */
associatedItemIds: string[]
local_url?: string
hosted_url?: string
offlineOnly: boolean
name: string
autoupdateDisabled: boolean
package_info: ComponentPackageInfo
area: ComponentArea
permissions: ComponentPermission[]
valid_until: Date
isMobileDefault: boolean
isDeprecated: boolean
isExplicitlyEnabledForItem(uuid: string): boolean
hasValidHostedUrl(): boolean
isTheme(): boolean
isExplicitlyDisabledForItem(uuid: string): boolean
legacyIsDefaultEditor(): boolean
get identifier(): FeatureIdentifier
get noteType(): NoteType
get displayName(): string
get deprecationMessage(): string | undefined
get thirdPartyPackageInfo(): ThirdPartyFeatureDescription
get isExpired(): boolean
/**
* @deprecated
* Replaced with active preferences managed by preferences service.
*/
legacyActive: boolean
/** @deprecated */
legacy_url?: string
/** @deprecated */
url?: string
/**
* @deprecated
* Replaced with per-note component data stored in the note's ComponentDataDomain.
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
legacyComponentData: Record<string, any>
}

View File

@@ -1,23 +1,15 @@
import { addIfUnique, removeFromArray } from '@standardnotes/utils'
import { ComponentPermission, FeatureDescription } from '@standardnotes/features'
import { ComponentFeatureDescription, ComponentPermission } from '@standardnotes/features'
import { AppDataField } from '../../Abstract/Item/Types/AppDataField'
import { ComponentContent } from './ComponentContent'
import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator'
export class ComponentMutator extends DecryptedItemMutator<ComponentContent> {
set active(active: boolean) {
this.mutableContent.active = active
}
set isMobileDefault(isMobileDefault: boolean) {
this.mutableContent.isMobileDefault = isMobileDefault
}
set componentData(componentData: Record<string, unknown>) {
this.mutableContent.componentData = componentData
}
set package_info(package_info: FeatureDescription) {
set package_info(package_info: ComponentFeatureDescription) {
this.mutableContent.package_info = package_info
}

View File

@@ -0,0 +1,172 @@
import {
AnyFeatureDescription,
ComponentArea,
ComponentPermission,
EditorFeatureDescription,
FeatureIdentifier,
IframeComponentFeatureDescription,
NoteType,
ThemeDockIcon,
UIFeatureDescriptionTypes,
isEditorFeatureDescription,
isIframeComponentFeatureDescription,
isThemeFeatureDescription,
} from '@standardnotes/features'
import { ComponentInterface } from './ComponentInterface'
import { isTheme } from '../Theme'
function isComponent(x: ComponentInterface | UIFeatureDescriptionTypes): x is ComponentInterface {
return 'uuid' in x
}
function isFeatureDescription(x: ComponentInterface | AnyFeatureDescription): x is AnyFeatureDescription {
return !('uuid' in x)
}
export function isIframeUIFeature(
x: ComponentOrNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>,
): x is ComponentOrNativeFeature<IframeComponentFeatureDescription> {
return isIframeComponentFeatureDescription(x.featureDescription)
}
export class ComponentOrNativeFeature<F extends UIFeatureDescriptionTypes> {
constructor(public readonly item: ComponentInterface | F) {}
get isComponent(): boolean {
return isComponent(this.item)
}
get isFeatureDescription(): boolean {
return isFeatureDescription(this.item)
}
get isThemeComponent(): boolean {
return isComponent(this.item) && isTheme(this.item)
}
get asComponent(): ComponentInterface {
if (isComponent(this.item)) {
return this.item
}
throw new Error('Cannot cast item to component')
}
get asFeatureDescription(): F {
if (isFeatureDescription(this.item)) {
return this.item
}
throw new Error('Cannot cast item to feature description')
}
get uniqueIdentifier(): string {
if (isFeatureDescription(this.item)) {
return this.item.identifier
} else {
return this.item.uuid
}
}
get featureIdentifier(): FeatureIdentifier {
return this.item.identifier
}
get noteType(): NoteType {
if (isFeatureDescription(this.item) && isEditorFeatureDescription(this.item)) {
return this.item.note_type ?? NoteType.Unknown
} else if (isComponent(this.item)) {
return this.item.noteType
}
throw new Error('Invalid component or feature description')
}
get fileType(): EditorFeatureDescription['file_type'] {
if (isFeatureDescription(this.item) && isEditorFeatureDescription(this.item)) {
return this.item.file_type
} else if (isComponent(this.item) && isEditorFeatureDescription(this.item.package_info)) {
return this.item.package_info?.file_type ?? 'txt'
}
throw new Error('Invalid component or feature description')
}
get displayName(): string {
if (isFeatureDescription(this.item)) {
return this.item.name ?? ''
} else {
return this.item.displayName
}
}
get description(): string {
if (isFeatureDescription(this.item)) {
return this.item.description ?? ''
} else {
return this.item.package_info.description ?? ''
}
}
get deprecationMessage(): string | undefined {
if (isFeatureDescription(this.item)) {
return this.item.deprecation_message
} else {
return this.item.deprecationMessage
}
}
get expirationDate(): Date | undefined {
if (isFeatureDescription(this.item)) {
return this.item.expires_at ? new Date(this.item.expires_at) : undefined
} else {
return this.item.valid_until
}
}
get featureDescription(): F {
if (isFeatureDescription(this.item)) {
return this.item
} else {
return this.item.package_info as F
}
}
get acquiredPermissions(): ComponentPermission[] {
if (isFeatureDescription(this.item) && isIframeComponentFeatureDescription(this.item)) {
return this.item.component_permissions ?? []
} else if (isComponent(this.item)) {
return this.item.permissions
}
throw new Error('Invalid component or feature description')
}
get area(): ComponentArea {
if ('area' in this.item) {
return this.item.area
}
return ComponentArea.Editor
}
get layerable(): boolean {
if (isComponent(this.item) && isTheme(this.item)) {
return this.item.layerable
} else if (isThemeFeatureDescription(this.asFeatureDescription)) {
return this.asFeatureDescription.layerable ?? false
}
return false
}
get dockIcon(): ThemeDockIcon | undefined {
if (isComponent(this.item) && isTheme(this.item)) {
return this.item.package_info.dock_icon
} else if (isThemeFeatureDescription(this.asFeatureDescription)) {
return this.asFeatureDescription.dock_icon
}
return undefined
}
}

View File

@@ -1,9 +1,9 @@
import { FeatureDescription, ThemeFeatureDescription } from '@standardnotes/features'
import { ComponentFeatureDescription, ThemeFeatureDescription } from '@standardnotes/features'
type ThirdPartyPackageInfo = {
version: string
download_url?: string
}
export type ComponentPackageInfo = FeatureDescription & Partial<ThirdPartyPackageInfo>
export type ThemePackageInfo = FeatureDescription & Partial<ThirdPartyPackageInfo> & ThemeFeatureDescription
export type ComponentPackageInfo = ComponentFeatureDescription & Partial<ThirdPartyPackageInfo>
export type ThemePackageInfo = ThemeFeatureDescription & Partial<ThirdPartyPackageInfo> & ThemeFeatureDescription

View File

@@ -1,3 +1,6 @@
export * from './Component'
export * from './ComponentMutator'
export * from './ComponentContent'
export * from './ComponentInterface'
export * from './ComponentOrNativeFeature'
export * from './PackageInfo'

View File

@@ -1,50 +1,18 @@
import { ComponentArea } from '@standardnotes/features'
import { SNComponent } from '../Component/Component'
import { ConflictStrategy } from '../../Abstract/Item/Types/ConflictStrategy'
import { AppDataField } from '../../Abstract/Item/Types/AppDataField'
import { HistoryEntryInterface } from '../../Runtime/History'
import { DecryptedItemInterface, ItemInterface } from '../../Abstract/Item'
import { ItemInterface } from '../../Abstract/Item'
import { ContentType } from '@standardnotes/domain-core'
import { useBoolean } from '@standardnotes/utils'
import { ThemePackageInfo } from '../Component/PackageInfo'
import { ContentType } from '@standardnotes/domain-core'
import { ThemeInterface } from './ThemeInterface'
export const isTheme = (x: ItemInterface): x is SNTheme => x.content_type === ContentType.TYPES.Theme
export const isTheme = (x: ItemInterface): x is ThemeInterface => x.content_type === ContentType.TYPES.Theme
export class SNTheme extends SNComponent {
export class SNTheme extends SNComponent implements ThemeInterface {
public override area: ComponentArea = ComponentArea.Themes
public declare readonly package_info: ThemePackageInfo
isLayerable(): boolean {
get layerable(): boolean {
return useBoolean(this.package_info && this.package_info.layerable, false)
}
/** Do not duplicate under most circumstances. Always keep original */
override strategyWhenConflictingWithItem(
_item: DecryptedItemInterface,
_previousRevision?: HistoryEntryInterface,
): ConflictStrategy {
return ConflictStrategy.KeepBase
}
getMobileRules() {
return (
this.getAppDomainValue(AppDataField.MobileRules) || {
constants: {},
rules: {},
}
)
}
/** Same as getMobileRules but without default value. */
hasMobileRules() {
return this.getAppDomainValue(AppDataField.MobileRules)
}
getNotAvailOnMobile() {
return this.getAppDomainValue(AppDataField.NotAvailableOnMobile)
}
isMobileActive() {
return this.getAppDomainValue(AppDataField.MobileActive)
}
}

View File

@@ -0,0 +1,7 @@
import { ComponentInterface } from '../Component'
import { ThemePackageInfo } from '../Component/PackageInfo'
export interface ThemeInterface extends ComponentInterface {
get layerable(): boolean
readonly package_info: ThemePackageInfo
}

View File

@@ -1,25 +1,4 @@
import { AppDataField } from '../../Abstract/Item/Types/AppDataField'
import { ComponentContent } from '../Component/ComponentContent'
import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator'
export class ThemeMutator extends DecryptedItemMutator<ComponentContent> {
setMobileRules(rules: unknown) {
this.setAppDataItem(AppDataField.MobileRules, rules)
}
setNotAvailOnMobile(notAvailable: boolean) {
this.setAppDataItem(AppDataField.NotAvailableOnMobile, notAvailable)
}
set local_url(local_url: string) {
this.mutableContent.local_url = local_url
}
/**
* We must not use .active because if you set that to true, it will also
* activate that theme on desktop/web
*/
setMobileActive(active: boolean) {
this.setAppDataItem(AppDataField.MobileActive, active)
}
}
export class ThemeMutator extends DecryptedItemMutator<ComponentContent> {}

View File

@@ -1,2 +1,3 @@
export * from './Theme'
export * from './ThemeMutator'
export * from './ThemeInterface'

View File

@@ -0,0 +1,7 @@
import { FeatureIdentifier } from '@standardnotes/features'
type UuidString = string
export type AllComponentPreferences = Record<FeatureIdentifier | UuidString, ComponentPreferencesEntry>
export type ComponentPreferencesEntry = Record<string, unknown>

View File

@@ -0,0 +1,7 @@
export enum EditorFontSize {
ExtraSmall = 'ExtraSmall',
Small = 'Small',
Normal = 'Normal',
Medium = 'Medium',
Large = 'Large',
}

View File

@@ -0,0 +1,8 @@
export enum EditorLineHeight {
None = 'None',
Tight = 'Tight',
Snug = 'Snug',
Normal = 'Normal',
Relaxed = 'Relaxed',
Loose = 'Loose',
}

View File

@@ -0,0 +1,6 @@
export enum EditorLineWidth {
Narrow = 'Narrow',
Wide = 'Wide',
Dynamic = 'Dynamic',
FullWidth = 'FullWidth',
}

View File

@@ -0,0 +1,6 @@
export enum NewNoteTitleFormat {
CurrentDateAndTime = 'CurrentDateAndTime',
CurrentNoteCount = 'CurrentNoteCount',
CustomFormat = 'CustomFormat',
Empty = 'Empty',
}

View File

@@ -1,13 +1,10 @@
import {
PrefKey,
CollectionSort,
NewNoteTitleFormat,
EditorLineHeight,
EditorFontSize,
EditorLineWidth,
PrefValue,
} from '@standardnotes/models'
import { FeatureIdentifier } from '@standardnotes/snjs'
import { FeatureIdentifier } from '@standardnotes/features'
import { CollectionSort } from '../../Runtime/Collection/CollectionSort'
import { EditorFontSize } from './EditorFontSize'
import { EditorLineHeight } from './EditorLineHeight'
import { EditorLineWidth } from './EditorLineWidth'
import { PrefKey, PrefValue } from './PrefKey'
import { NewNoteTitleFormat } from './NewNoteTitleFormat'
export const PrefDefaults = {
[PrefKey.TagsPanelWidth]: 220,
@@ -44,6 +41,9 @@ export const PrefDefaults = {
[PrefKey.SuperNoteExportFormat]: 'json',
[PrefKey.SystemViewPreferences]: {},
[PrefKey.AuthenticatorNames]: '',
[PrefKey.ComponentPreferences]: {},
[PrefKey.ActiveThemes]: [],
[PrefKey.ActiveComponents]: [],
} satisfies {
[key in PrefKey]: PrefValue[key]
}

View File

@@ -2,6 +2,11 @@ import { CollectionSortProperty } from '../../Runtime/Collection/CollectionSort'
import { EditorIdentifier, FeatureIdentifier } from '@standardnotes/features'
import { SystemViewId } from '../SmartView'
import { TagPreferences } from '../Tag'
import { NewNoteTitleFormat } from './NewNoteTitleFormat'
import { EditorLineHeight } from './EditorLineHeight'
import { EditorLineWidth } from './EditorLineWidth'
import { EditorFontSize } from './EditorFontSize'
import { AllComponentPreferences } from './ComponentPreferences'
export enum PrefKey {
TagsPanelWidth = 'tagsPanelWidth',
@@ -38,37 +43,9 @@ export enum PrefKey {
SuperNoteExportFormat = 'superNoteExportFormat',
AuthenticatorNames = 'authenticatorNames',
PaneGesturesEnabled = 'paneGesturesEnabled',
}
export enum NewNoteTitleFormat {
CurrentDateAndTime = 'CurrentDateAndTime',
CurrentNoteCount = 'CurrentNoteCount',
CustomFormat = 'CustomFormat',
Empty = 'Empty',
}
export enum EditorLineHeight {
None = 'None',
Tight = 'Tight',
Snug = 'Snug',
Normal = 'Normal',
Relaxed = 'Relaxed',
Loose = 'Loose',
}
export enum EditorLineWidth {
Narrow = 'Narrow',
Wide = 'Wide',
Dynamic = 'Dynamic',
FullWidth = 'FullWidth',
}
export enum EditorFontSize {
ExtraSmall = 'ExtraSmall',
Small = 'Small',
Normal = 'Normal',
Medium = 'Medium',
Large = 'Large',
ComponentPreferences = 'componentPreferences',
ActiveThemes = 'activeThemes',
ActiveComponents = 'activeComponents',
}
export type PrefValue = {
@@ -106,4 +83,7 @@ export type PrefValue = {
[PrefKey.SuperNoteExportFormat]: 'json' | 'md' | 'html'
[PrefKey.AuthenticatorNames]: string
[PrefKey.PaneGesturesEnabled]: boolean
[PrefKey.ComponentPreferences]: AllComponentPreferences
[PrefKey.ActiveThemes]: string[]
[PrefKey.ActiveComponents]: string[]
}

View File

@@ -1,3 +1,9 @@
export * from './UserPrefs'
export * from './UserPrefsMutator'
export * from './PrefKey'
export * from './EditorLineHeight'
export * from './EditorFontSize'
export * from './EditorLineWidth'
export * from './NewNoteTitleFormat'
export * from './ComponentPreferences'
export * from './PrefDefaults'

View File

@@ -1,4 +1,4 @@
import { FeatureDescription } from '@standardnotes/features'
import { AnyFeatureDescription } from '@standardnotes/features'
import { SubscriptionName } from '@standardnotes/common'
export type AvailableSubscriptions = {
@@ -8,6 +8,6 @@ export type AvailableSubscriptions = {
price: number
period: string
}[]
features: FeatureDescription[]
features: AnyFeatureDescription[]
}
}

View File

@@ -1,6 +1,6 @@
import { FeatureDescription } from '@standardnotes/features'
import { AnyFeatureDescription } from '@standardnotes/features'
export type GetOfflineFeaturesResponse = {
features: FeatureDescription[]
features: AnyFeatureDescription[]
roles: string[]
}

View File

@@ -1,5 +0,0 @@
import { FeatureDescription } from '@standardnotes/features'
export type UserFeaturesData = {
features: FeatureDescription[]
}

View File

@@ -1,3 +0,0 @@
import { UserFeaturesData } from './UserFeaturesData'
export type UserFeaturesResponse = UserFeaturesData

View File

@@ -61,8 +61,6 @@ export * from './User/ListSettingsResponse'
export * from './User/PostSubscriptionTokensResponse'
export * from './User/SettingData'
export * from './User/UpdateSettingResponse'
export * from './User/UserFeaturesData'
export * from './User/UserFeaturesResponse'
export * from './UserEvent/UserEventServerHash'
export * from './UserEvent/UserEventType'

View File

@@ -0,0 +1,6 @@
/* istanbul ignore file */
export enum ApiServiceEvent {
MetaReceived = 'MetaReceived',
SessionRefreshed = 'SessionRefreshed',
}

View File

@@ -0,0 +1,5 @@
import { Either } from '@standardnotes/common'
import { SessionRefreshedData } from './SessionRefreshedData'
import { MetaReceivedData } from './MetaReceivedData'
export type ApiServiceEventData = Either<MetaReceivedData, SessionRefreshedData>

View File

@@ -1,26 +1,17 @@
import { Either } from '@standardnotes/common'
import { FilesApiInterface } from '@standardnotes/files'
import { Session } from '@standardnotes/domain-core'
import { Role } from '@standardnotes/security'
import { AbstractService } from '../Service/AbstractService'
import { ApiServiceEvent } from './ApiServiceEvent'
import { ApiServiceEventData } from './ApiServiceEventData'
import { SNFeatureRepo } from '@standardnotes/models'
import { ClientDisplayableError, HttpResponse } from '@standardnotes/responses'
import { AnyFeatureDescription } from '@standardnotes/features'
/* istanbul ignore file */
export interface ApiServiceInterface extends AbstractService<ApiServiceEvent, ApiServiceEventData>, FilesApiInterface {
isThirdPartyHostUsed(): boolean
export enum ApiServiceEvent {
MetaReceived = 'MetaReceived',
SessionRefreshed = 'SessionRefreshed',
downloadOfflineFeaturesFromRepo(
repo: SNFeatureRepo,
): Promise<{ features: AnyFeatureDescription[]; roles: string[] } | ClientDisplayableError>
downloadFeatureUrl(url: string): Promise<HttpResponse>
}
export type MetaReceivedData = {
userUuid: string
userRoles: Role[]
}
export type SessionRefreshedData = {
session: Session
}
export type ApiServiceEventData = Either<MetaReceivedData, SessionRefreshedData>
export interface ApiServiceInterface extends AbstractService<ApiServiceEvent, ApiServiceEventData>, FilesApiInterface {}

View File

@@ -0,0 +1,6 @@
import { Role } from '@standardnotes/security'
export type MetaReceivedData = {
userUuid: string
userRoles: Role[]
}

View File

@@ -0,0 +1,5 @@
import { Session } from '@standardnotes/domain-core'
export type SessionRefreshedData = {
session: Session
}

View File

@@ -1,3 +1,4 @@
import { PreferenceServiceInterface } from './../Preferences/PreferenceServiceInterface'
import { AsymmetricMessageServiceInterface } from './../AsymmetricMessage/AsymmetricMessageServiceInterface'
import { SyncOptions } from './../Sync/SyncOptions'
import { ImportDataReturnType } from './../Mutator/ImportDataUseCase'
@@ -21,7 +22,7 @@ import { ComponentManagerInterface } from '../Component/ComponentManagerInterfac
import { ApplicationEvent } from '../Event/ApplicationEvent'
import { ApplicationEventCallback } from '../Event/ApplicationEventCallback'
import { FeaturesClientInterface } from '../Feature/FeaturesClientInterface'
import { SubscriptionClientInterface } from '../Subscription/SubscriptionClientInterface'
import { SubscriptionManagerInterface } from '../Subscription/SubscriptionManagerInterface'
import { DeviceInterface } from '../Device/DeviceInterface'
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
import { MutatorClientInterface } from '../Mutator/MutatorClientInterface'
@@ -66,6 +67,7 @@ export interface ApplicationInterface {
setCustomHost(host: string): Promise<void>
isThirdPartyHostUsed(): boolean
isUsingHomeServer(): Promise<boolean>
getNewSubscriptionToken(): Promise<string | undefined>
importData(data: BackupFile, awaitSync?: boolean): Promise<ImportDataReturnType>
/**
@@ -96,7 +98,7 @@ export interface ApplicationInterface {
get mutator(): MutatorClientInterface
get user(): UserClientInterface
get files(): FilesClientInterface
get subscriptions(): SubscriptionClientInterface
get subscriptions(): SubscriptionManagerInterface
get fileBackups(): BackupServiceInterface | undefined
get sessions(): SessionsClientInterface
get homeServer(): HomeServerServiceInterface | undefined
@@ -104,6 +106,7 @@ export interface ApplicationInterface {
get challenges(): ChallengeServiceInterface
get alerts(): AlertService
get asymmetric(): AsymmetricMessageServiceInterface
get preferences(): PreferenceServiceInterface
readonly identifier: ApplicationIdentifier
readonly platform: Platform

View File

@@ -1,26 +1,47 @@
import { ComponentArea, FeatureIdentifier } from '@standardnotes/features'
import { ActionObserver, PermissionDialog, SNComponent, SNNote } from '@standardnotes/models'
import { ComponentViewerItem } from './ComponentViewerItem'
import {
ComponentArea,
ComponentFeatureDescription,
EditorFeatureDescription,
IframeComponentFeatureDescription,
ThemeFeatureDescription,
} from '@standardnotes/features'
import {
ActionObserver,
ComponentInterface,
ComponentOrNativeFeature,
PermissionDialog,
SNNote,
} from '@standardnotes/models'
import { DesktopManagerInterface } from '../Device/DesktopManagerInterface'
import { ComponentViewerInterface } from './ComponentViewerInterface'
export interface ComponentManagerInterface {
urlForComponent(component: SNComponent): string | undefined
urlForComponent(uiFeature: ComponentOrNativeFeature<ComponentFeatureDescription>): string | undefined
setDesktopManager(desktopManager: DesktopManagerInterface): void
componentsForArea(area: ComponentArea): SNComponent[]
editorForNote(note: SNNote): SNComponent | undefined
doesEditorChangeRequireAlert(from: SNComponent | undefined, to: SNComponent | undefined): boolean
thirdPartyComponentsForArea(area: ComponentArea): ComponentInterface[]
editorForNote(note: SNNote): ComponentOrNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>
doesEditorChangeRequireAlert(
from: ComponentOrNativeFeature<IframeComponentFeatureDescription | EditorFeatureDescription> | undefined,
to: ComponentOrNativeFeature<IframeComponentFeatureDescription | EditorFeatureDescription> | undefined,
): boolean
showEditorChangeAlert(): Promise<boolean>
destroyComponentViewer(viewer: ComponentViewerInterface): void
createComponentViewer(
component: SNComponent,
contextItem?: string,
uiFeature: ComponentOrNativeFeature<IframeComponentFeatureDescription>,
item: ComponentViewerItem,
actionObserver?: ActionObserver,
urlOverride?: string,
): ComponentViewerInterface
presentPermissionsDialog(_dialog: PermissionDialog): void
legacyGetDefaultEditor(): SNComponent | undefined
componentWithIdentifier(identifier: FeatureIdentifier | string): SNComponent | undefined
toggleTheme(uuid: string): Promise<void>
toggleComponent(uuid: string): Promise<void>
legacyGetDefaultEditor(): ComponentInterface | undefined
isThemeActive(theme: ComponentOrNativeFeature<ThemeFeatureDescription>): boolean
toggleTheme(theme: ComponentOrNativeFeature<ThemeFeatureDescription>): Promise<void>
getActiveThemes(): ComponentOrNativeFeature<ThemeFeatureDescription>[]
getActiveThemesIdentifiers(): string[]
isComponentActive(component: ComponentInterface): boolean
toggleComponent(component: ComponentInterface): Promise<void>
}

View File

@@ -2,20 +2,22 @@ import {
ActionObserver,
ComponentEventObserver,
ComponentMessage,
DecryptedItemInterface,
SNComponent,
ComponentOrNativeFeature,
} from '@standardnotes/models'
import { FeatureStatus } from '../Feature/FeatureStatus'
import { ComponentViewerError } from './ComponentViewerError'
import { IframeComponentFeatureDescription } from '@standardnotes/features'
export interface ComponentViewerInterface {
readonly component: SNComponent
readonly url?: string
identifier: string
lockReadonly: boolean
sessionKey?: string
overrideContextItem?: DecryptedItemInterface
get componentUuid(): string
readonly identifier: string
readonly lockReadonly: boolean
readonly sessionKey?: string
get url(): string
get componentUniqueIdentifier(): string
getComponentOrFeatureItem(): ComponentOrNativeFeature<IframeComponentFeatureDescription>
destroy(): void
setReadonly(readonly: boolean): void
getFeatureStatus(): FeatureStatus

View File

@@ -0,0 +1,9 @@
import { DecryptedItemInterface } from '@standardnotes/models'
export type ComponentViewerItem = { uuid: string } | { readonlyItem: DecryptedItemInterface }
export function isComponentViewerItemReadonlyItem(
item: ComponentViewerItem,
): item is { readonlyItem: DecryptedItemInterface } {
return 'readonlyItem' in item
}

View File

@@ -1,7 +1,7 @@
import { SNComponent } from '@standardnotes/models'
import { ComponentInterface } from '@standardnotes/models'
export interface DesktopManagerInterface {
syncComponentsInstallation(components: SNComponent[]): void
registerUpdateObserver(callback: (component: SNComponent) => void): () => void
syncComponentsInstallation(components: ComponentInterface[]): void
registerUpdateObserver(callback: (component: ComponentInterface) => void): () => void
getExtServerHost(): string
}

View File

@@ -15,6 +15,7 @@ export interface MobileDeviceInterface extends DeviceInterface {
authenticateWithBiometrics(): Promise<boolean>
hideMobileInterfaceFromScreenshots(): void
stopHidingMobileInterfaceFromScreenshots(): void
// eslint-disable-next-line @typescript-eslint/no-explicit-any
consoleLog(...args: any[]): void
handleThemeSchemeChange(isDark: boolean, bgColor: string): void
shareBase64AsFile(base64: string, filename: string): Promise<void>

View File

@@ -1,79 +1,61 @@
import { ApplicationStage } from './../Application/ApplicationStage'
export enum ApplicationEvent {
SignedIn = 'signed-in',
SignedOut = 'signed-out',
SignedIn = 'Application:SignedIn',
SignedOut = 'Application:SignedOut',
/** When a full, potentially multi-page sync completes */
CompletedFullSync = 'completed-full-sync',
FailedSync = 'failed-sync',
HighLatencySync = 'high-latency-sync',
EnteredOutOfSync = 'entered-out-of-sync',
ExitedOutOfSync = 'exited-out-of-sync',
ApplicationStageChanged = 'application-stage-changed',
CompletedFullSync = 'Application:CompletedFullSync',
FailedSync = 'Application:FailedSync',
HighLatencySync = 'Application:HighLatencySync',
EnteredOutOfSync = 'Application:EnteredOutOfSync',
ExitedOutOfSync = 'Application:ExitedOutOfSync',
ApplicationStageChanged = 'Application:ApplicationStageChanged',
/**
* The application has finished its prepareForLaunch state and is now ready for unlock
* Called when the application has initialized and is ready for launch, but before
* the application has been unlocked, if applicable. Use this to do pre-launch
* configuration, but do not attempt to access user data like notes or tags.
*/
Started = 'started',
Started = 'Application:Started',
/**
* The applicaiton is fully unlocked and ready for i/o
* Called when the application has been fully decrypted and unlocked. Use this to
* to begin streaming data like notes and tags.
*/
Launched = 'launched',
LocalDataLoaded = 'local-data-loaded',
Launched = 'Application:Launched',
LocalDataLoaded = 'Application:LocalDataLoaded',
/**
* When the root key or root key wrapper changes. Includes events like account state
* changes (registering, signing in, changing pw, logging out) and passcode state
* changes (adding, removing, changing).
*/
KeyStatusChanged = 'key-status-changed',
MajorDataChange = 'major-data-change',
CompletedRestart = 'completed-restart',
LocalDataIncrementalLoad = 'local-data-incremental-load',
SyncStatusChanged = 'sync-status-changed',
WillSync = 'will-sync',
InvalidSyncSession = 'invalid-sync-session',
LocalDatabaseReadError = 'local-database-read-error',
LocalDatabaseWriteError = 'local-database-write-error',
KeyStatusChanged = 'Application:KeyStatusChanged',
MajorDataChange = 'Application:MajorDataChange',
CompletedRestart = 'Application:CompletedRestart',
LocalDataIncrementalLoad = 'Application:LocalDataIncrementalLoad',
SyncStatusChanged = 'Application:SyncStatusChanged',
WillSync = 'Application:WillSync',
InvalidSyncSession = 'Application:InvalidSyncSession',
LocalDatabaseReadError = 'Application:LocalDatabaseReadError',
LocalDatabaseWriteError = 'Application:LocalDatabaseWriteError',
/**
* When a single roundtrip completes with sync, in a potentially multi-page sync request.
* If just a single roundtrip, this event will be triggered, along with CompletedFullSync
*/
CompletedIncrementalSync = 'completed-incremental-sync',
CompletedIncrementalSync = 'Application:CompletedIncrementalSync',
/**
* The application has loaded all pending migrations (but not run any, except for the base one),
* and consumers may now call hasPendingMigrations
*/
MigrationsLoaded = 'migrations-loaded',
MigrationsLoaded = 'Application:MigrationsLoaded',
/** When StorageService is ready (but NOT yet decrypted) to start servicing read/write requests */
StorageReady = 'storage-ready',
PreferencesChanged = 'preferences-changed',
UnprotectedSessionBegan = 'unprotected-session-began',
UserRolesChanged = 'user-roles-changed',
FeaturesUpdated = 'features-updated',
UnprotectedSessionExpired = 'unprotected-session-expired',
StorageReady = 'Application:StorageReady',
PreferencesChanged = 'Application:PreferencesChanged',
UnprotectedSessionBegan = 'Application:UnprotectedSessionBegan',
UserRolesChanged = 'Application:UserRolesChanged',
FeaturesAvailabilityChanged = 'Application:FeaturesAvailabilityChanged',
UnprotectedSessionExpired = 'Application:UnprotectedSessionExpired',
/** Called when the app first launches and after first sync request made after sign in */
CompletedInitialSync = 'completed-initial-sync',
BiometricsSoftLockEngaged = 'biometrics-soft-lock-engaged',
BiometricsSoftLockDisengaged = 'biometrics-soft-lock-disengaged',
DidPurchaseSubscription = 'did-purchase-subscription',
}
export type ApplicationStageChangedEventPayload = {
stage: ApplicationStage
CompletedInitialSync = 'Application:CompletedInitialSync',
BiometricsSoftLockEngaged = 'Application:BiometricsSoftLockEngaged',
BiometricsSoftLockDisengaged = 'Application:BiometricsSoftLockDisengaged',
DidPurchaseSubscription = 'Application:DidPurchaseSubscription',
}

View File

@@ -0,0 +1,5 @@
import { ApplicationStage } from './../Application/ApplicationStage'
export type ApplicationStageChangedEventPayload = {
stage: ApplicationStage
}

View File

@@ -1,37 +1,27 @@
import { FeatureIdentifier } from '@standardnotes/features'
import { SNComponent } from '@standardnotes/models'
import { ComponentInterface } from '@standardnotes/models'
import { FeatureStatus } from './FeatureStatus'
import { SetOfflineFeaturesFunctionResponse } from './SetOfflineFeaturesFunctionResponse'
export interface FeaturesClientInterface {
downloadExternalFeature(urlOrCode: string): Promise<SNComponent | undefined>
getFeatureStatus(featureId: FeatureIdentifier): FeatureStatus
hasFirstPartySubscription(): boolean
hasMinimumRole(role: string): boolean
hasFirstPartyOfflineSubscription(): boolean
setOfflineFeaturesCode(code: string): Promise<SetOfflineFeaturesFunctionResponse>
hasOfflineRepo(): boolean
deleteOfflineFeatureRepo(): Promise<void>
isThirdPartyFeature(identifier: string): boolean
toggleExperimentalFeature(identifier: FeatureIdentifier): void
getExperimentalFeatures(): FeatureIdentifier[]
getEnabledExperimentalFeatures(): FeatureIdentifier[]
enableExperimentalFeature(identifier: FeatureIdentifier): void
disableExperimentalFeature(identifier: FeatureIdentifier): void
isExperimentalFeatureEnabled(identifier: FeatureIdentifier): boolean
isExperimentalFeature(identifier: FeatureIdentifier): boolean
downloadRemoteThirdPartyFeature(urlOrCode: string): Promise<ComponentInterface | undefined>
}

View File

@@ -1,5 +1,5 @@
export enum FeaturesEvent {
UserRolesChanged = 'UserRolesChanged',
FeaturesUpdated = 'FeaturesUpdated',
FeaturesAvailabilityChanged = 'Features:FeaturesAvailabilityChanged',
DidPurchaseSubscription = 'DidPurchaseSubscription',
}

View File

@@ -1,3 +1,3 @@
import { ClientDisplayableError } from '@standardnotes/responses'
export type SetOfflineFeaturesFunctionResponse = ClientDisplayableError | undefined
export type SetOfflineFeaturesFunctionResponse = ClientDisplayableError | void

View File

@@ -15,13 +15,13 @@ import {
SNNote,
SmartView,
TagItemCountChangeObserver,
SNComponent,
SNTheme,
DecryptedPayloadInterface,
DecryptedTransferPayload,
FileItem,
VaultDisplayOptions,
NotesAndFilesDisplayControllerOptions,
ThemeInterface,
ComponentInterface,
} from '@standardnotes/models'
import { AbstractService } from '../Service/AbstractService'
@@ -92,9 +92,15 @@ export interface ItemManagerInterface extends AbstractService {
itemToLookupUuidFor: DecryptedItemInterface,
contentType?: string,
): I[]
findItem<T extends DecryptedItemInterface = DecryptedItemInterface>(uuid: string): T | undefined
findItems<T extends DecryptedItemInterface>(uuids: string[]): T[]
findSureItem<T extends DecryptedItemInterface = DecryptedItemInterface>(uuid: string): T
/**
* If item is not found, an `undefined` element will be inserted into the array.
*/
findItemsIncludingBlanks<T extends DecryptedItemInterface>(uuids: string[]): (T | undefined)[]
get trashedItems(): SNNote[]
itemsBelongingToKeySystem(systemIdentifier: KeySystemIdentifier): DecryptedItemInterface[]
hasTagsNeedingFoldersMigration(): boolean
@@ -111,8 +117,8 @@ export interface ItemManagerInterface extends AbstractService {
getTagParent(itemToLookupUuidFor: SNTag): SNTag | undefined
isValidTagParent(parentTagToLookUpUuidFor: SNTag, childToLookUpUuidFor: SNTag): boolean
isSmartViewTitle(title: string): boolean
getDisplayableComponents(): (SNComponent | SNTheme)[]
createItemFromPayload(payload: DecryptedPayloadInterface): DecryptedItemInterface
getDisplayableComponents(): (ComponentInterface | ThemeInterface)[]
createItemFromPayload<T extends DecryptedItemInterface>(payload: DecryptedPayloadInterface): T
createPayloadFromObject(object: DecryptedTransferPayload): DecryptedPayloadInterface
getDisplayableFiles(): FileItem[]
setVaultDisplayOptions(options: VaultDisplayOptions): void

View File

@@ -1,4 +1,5 @@
import {
ComponentInterface,
ComponentMutator,
DecryptedItemInterface,
DecryptedItemMutator,
@@ -13,7 +14,6 @@ import {
PayloadEmitSource,
PredicateInterface,
SmartView,
SNComponent,
SNFeatureRepo,
SNNote,
SNTag,
@@ -72,12 +72,12 @@ export interface MutatorClientInterface {
): Promise<ItemsKeyInterface>
changeComponent(
itemToLookupUuidFor: SNComponent,
itemToLookupUuidFor: ComponentInterface,
mutate: (mutator: ComponentMutator) => void,
mutationType?: MutationType,
emitSource?: PayloadEmitSource,
payloadSourceKey?: string,
): Promise<SNComponent>
): Promise<ComponentInterface>
changeFeatureRepo(
itemToLookupUuidFor: SNFeatureRepo,

View File

@@ -1,8 +1,6 @@
import { PrefKey, PrefValue } from '@standardnotes/models'
import { AbstractService } from '../Service/AbstractService'
/* istanbul ignore file */
export enum PreferencesServiceEvent {
PreferencesChanged = 'PreferencesChanged',
}
@@ -11,5 +9,8 @@ export interface PreferenceServiceInterface extends AbstractService<PreferencesS
getValue<K extends PrefKey>(key: K, defaultValue: PrefValue[K] | undefined): PrefValue[K] | undefined
getValue<K extends PrefKey>(key: K, defaultValue: PrefValue[K]): PrefValue[K]
getValue<K extends PrefKey>(key: K, defaultValue?: PrefValue[K]): PrefValue[K] | undefined
setValue<K extends PrefKey>(key: K, value: PrefValue[K]): Promise<void>
/** Set value without triggering sync or event notifications */
setValueDetached<K extends PrefKey>(key: K, value: PrefValue[K]): Promise<void>
}

View File

@@ -15,6 +15,7 @@ export interface SessionsClientInterface {
isSignedIn(): boolean
get userUuid(): string
getSureUser(): User
isSignedIntoFirstPartyServer(): boolean
isCurrentSessionReadOnly(): boolean | undefined
register(email: string, password: string, ephemeral: boolean): Promise<UserRegistrationResponseBody>

View File

@@ -36,7 +36,6 @@ export enum StorageKey {
WebSocketUrl = 'webSocket_url',
UserRoles = 'user_roles',
OfflineUserRoles = 'offline_user_roles',
UserFeatures = 'user_features',
ExperimentalFeatures = 'experimental_features',
DeinitMode = 'deinit_mode',
CodeVerifier = 'code_verifier',
@@ -50,6 +49,7 @@ export enum StorageKey {
FileBackupsEnabled = 'file_backups_enabled',
FileBackupsLocation = 'file_backups_location',
VaultSelectionOptions = 'vault_selection_options',
Subscription = 'subscription',
}
export enum NonwrappedStorageKey {

View File

@@ -1,13 +0,0 @@
import { Invitation } from '@standardnotes/models'
import { AppleIAPReceipt } from './AppleIAPReceipt'
export interface SubscriptionClientInterface {
listSubscriptionInvitations(): Promise<Invitation[]>
inviteToSubscription(inviteeEmail: string): Promise<boolean>
cancelInvitation(inviteUuid: string): Promise<boolean>
acceptInvitation(inviteUuid: string): Promise<{ success: true } | { success: false; message: string }>
confirmAppleIAP(
receipt: AppleIAPReceipt,
subscriptionToken: string,
): Promise<{ success: true } | { success: false; message: string }>
}

View File

@@ -1,3 +1,5 @@
import { StorageServiceInterface } from './../Storage/StorageServiceInterface'
import { SessionsClientInterface } from './../Session/SessionsClientInterface'
import { SubscriptionApiServiceInterface } from '@standardnotes/api'
import { Invitation } from '@standardnotes/models'
import { InternalEventBusInterface } from '..'
@@ -6,8 +8,10 @@ import { SubscriptionManager } from './SubscriptionManager'
describe('SubscriptionManager', () => {
let subscriptionApiService: SubscriptionApiServiceInterface
let internalEventBus: InternalEventBusInterface
let sessions: SessionsClientInterface
let storage: StorageServiceInterface
const createManager = () => new SubscriptionManager(subscriptionApiService, internalEventBus)
const createManager = () => new SubscriptionManager(subscriptionApiService, sessions, storage, internalEventBus)
beforeEach(() => {
subscriptionApiService = {} as jest.Mocked<SubscriptionApiServiceInterface>
@@ -16,7 +20,12 @@ describe('SubscriptionManager', () => {
subscriptionApiService.invite = jest.fn()
subscriptionApiService.listInvites = jest.fn()
sessions = {} as jest.Mocked<SessionsClientInterface>
storage = {} as jest.Mocked<StorageServiceInterface>
internalEventBus = {} as jest.Mocked<InternalEventBusInterface>
internalEventBus.addEventHandler = jest.fn()
})
it('should invite user by email to a shared subscription', async () => {

View File

@@ -1,17 +1,117 @@
import { StorageKey } from './../Storage/StorageKeys'
import { ApplicationStage } from './../Application/ApplicationStage'
import { StorageServiceInterface } from './../Storage/StorageServiceInterface'
import { InternalEventInterface } from './../Internal/InternalEventInterface'
import { SessionsClientInterface } from './../Session/SessionsClientInterface'
import { SubscriptionName } from '@standardnotes/common'
import { convertTimestampToMilliseconds } from '@standardnotes/utils'
import { ApplicationEvent } from './../Event/ApplicationEvent'
import { InternalEventHandlerInterface } from './../Internal/InternalEventHandlerInterface'
import { Invitation } from '@standardnotes/models'
import { SubscriptionApiServiceInterface } from '@standardnotes/api'
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
import { AbstractService } from '../Service/AbstractService'
import { SubscriptionClientInterface } from './SubscriptionClientInterface'
import { SubscriptionManagerInterface } from './SubscriptionManagerInterface'
import { AppleIAPReceipt } from './AppleIAPReceipt'
import { getErrorFromErrorResponse, isErrorResponse } from '@standardnotes/responses'
import { AvailableSubscriptions, getErrorFromErrorResponse, isErrorResponse } from '@standardnotes/responses'
import { Subscription } from '@standardnotes/security'
import { SubscriptionManagerEvent } from './SubscriptionManagerEvent'
export class SubscriptionManager
extends AbstractService<SubscriptionManagerEvent>
implements SubscriptionManagerInterface, InternalEventHandlerInterface
{
private onlineSubscription?: Subscription
private availableSubscriptions?: AvailableSubscriptions | undefined
export class SubscriptionManager extends AbstractService implements SubscriptionClientInterface {
constructor(
private subscriptionApiService: SubscriptionApiServiceInterface,
private sessions: SessionsClientInterface,
private storage: StorageServiceInterface,
protected override internalEventBus: InternalEventBusInterface,
) {
super(internalEventBus)
internalEventBus.addEventHandler(this, ApplicationEvent.UserRolesChanged)
internalEventBus.addEventHandler(this, ApplicationEvent.Launched)
internalEventBus.addEventHandler(this, ApplicationEvent.SignedIn)
}
async handleEvent(event: InternalEventInterface): Promise<void> {
switch (event.type) {
case ApplicationEvent.Launched: {
void this.fetchOnlineSubscription()
void this.fetchAvailableSubscriptions()
break
}
case ApplicationEvent.UserRolesChanged:
case ApplicationEvent.SignedIn:
void this.fetchOnlineSubscription()
break
}
}
public override async handleApplicationStage(stage: ApplicationStage): Promise<void> {
if (stage === ApplicationStage.StorageDecrypted_09) {
this.onlineSubscription = this.storage.getValue(StorageKey.Subscription)
void this.notifyEvent(SubscriptionManagerEvent.DidFetchSubscription)
}
}
hasOnlineSubscription(): boolean {
return this.onlineSubscription != undefined
}
getOnlineSubscription(): Subscription | undefined {
return this.onlineSubscription
}
getAvailableSubscriptions(): AvailableSubscriptions | undefined {
return this.availableSubscriptions
}
get userSubscriptionName(): string {
if (!this.onlineSubscription) {
throw new Error('Attempting to get subscription name without a subscription.')
}
if (
this.availableSubscriptions &&
this.availableSubscriptions[this.onlineSubscription.planName as SubscriptionName]
) {
return this.availableSubscriptions[this.onlineSubscription.planName as SubscriptionName].name
}
return ''
}
get userSubscriptionExpirationDate(): Date | undefined {
if (!this.onlineSubscription) {
return undefined
}
return new Date(convertTimestampToMilliseconds(this.onlineSubscription.endsAt))
}
get isUserSubscriptionExpired(): boolean {
if (!this.onlineSubscription) {
throw new Error('Attempting to check subscription expiration without a subscription.')
}
if (!this.userSubscriptionExpirationDate) {
return false
}
return this.userSubscriptionExpirationDate.getTime() < new Date().getTime()
}
get isUserSubscriptionCanceled(): boolean {
if (!this.onlineSubscription) {
throw new Error('Attempting to check subscription expiration without a subscription.')
}
return this.onlineSubscription.cancelled
}
async acceptInvitation(inviteUuid: string): Promise<{ success: true } | { success: false; message: string }> {
@@ -70,6 +170,48 @@ export class SubscriptionManager extends AbstractService implements Subscription
}
}
private async fetchOnlineSubscription(): Promise<void> {
if (!this.sessions.isSignedIn()) {
return
}
try {
const result = await this.subscriptionApiService.getUserSubscription({ userUuid: this.sessions.userUuid })
if (isErrorResponse(result)) {
return
}
const subscription = result.data.subscription
this.handleReceivedOnlineSubscriptionFromServer(subscription)
} catch (error) {
void error
}
}
private handleReceivedOnlineSubscriptionFromServer(subscription: Subscription | undefined): void {
this.onlineSubscription = subscription
this.storage.setValue(StorageKey.Subscription, subscription)
void this.notifyEvent(SubscriptionManagerEvent.DidFetchSubscription)
}
private async fetchAvailableSubscriptions(): Promise<void> {
try {
const response = await this.subscriptionApiService.getAvailableSubscriptions()
if (isErrorResponse(response)) {
return
}
this.availableSubscriptions = response.data
} catch (error) {
void error
}
}
async confirmAppleIAP(
params: AppleIAPReceipt,
subscriptionToken: string,

View File

@@ -0,0 +1,3 @@
export enum SubscriptionManagerEvent {
DidFetchSubscription = 'Subscription:DidFetchSubscription',
}

View File

@@ -0,0 +1,26 @@
import { ApplicationServiceInterface } from './../Service/ApplicationServiceInterface'
import { Invitation } from '@standardnotes/models'
import { AppleIAPReceipt } from './AppleIAPReceipt'
import { AvailableSubscriptions } from '@standardnotes/responses'
import { Subscription } from '@standardnotes/security'
import { SubscriptionManagerEvent } from './SubscriptionManagerEvent'
export interface SubscriptionManagerInterface extends ApplicationServiceInterface<SubscriptionManagerEvent, unknown> {
getOnlineSubscription(): Subscription | undefined
getAvailableSubscriptions(): AvailableSubscriptions | undefined
hasOnlineSubscription(): boolean
get userSubscriptionName(): string
get userSubscriptionExpirationDate(): Date | undefined
get isUserSubscriptionExpired(): boolean
get isUserSubscriptionCanceled(): boolean
listSubscriptionInvitations(): Promise<Invitation[]>
inviteToSubscription(inviteeEmail: string): Promise<boolean>
cancelInvitation(inviteUuid: string): Promise<boolean>
acceptInvitation(inviteUuid: string): Promise<{ success: true } | { success: false; message: string }>
confirmAppleIAP(
receipt: AppleIAPReceipt,
subscriptionToken: string,
): Promise<{ success: true } | { success: false; message: string }>
}

View File

@@ -0,0 +1,4 @@
export enum AccountEvent {
SignedInOrRegistered = 'SignedInOrRegistered',
SignedOut = 'SignedOut',
}

View File

@@ -0,0 +1,7 @@
import { Either } from '@standardnotes/common'
import { SignedInOrRegisteredEventPayload } from './SignedInOrRegisteredEventPayload'
import { SignedOutEventPayload } from './SignedOutEventPayload'
export interface AccountEventData {
payload: Either<SignedInOrRegisteredEventPayload, SignedOutEventPayload>
}

View File

@@ -0,0 +1,3 @@
import { HttpError } from '@standardnotes/responses'
export type CredentialsChangeFunctionResponse = { error?: HttpError }

View File

@@ -0,0 +1,6 @@
export interface SignedInOrRegisteredEventPayload {
ephemeral: boolean
mergeLocal: boolean
awaitSync: boolean
checkIntegrity: boolean
}

View File

@@ -0,0 +1,5 @@
import { DeinitSource } from '../Application/DeinitSource'
export interface SignedOutEventPayload {
source: DeinitSource
}

View File

@@ -1,33 +1,14 @@
import { Base64String } from '@standardnotes/sncrypto-common'
import { Either, UserRequestType } from '@standardnotes/common'
import { UserRequestType } from '@standardnotes/common'
import { DeinitSource } from '../Application/DeinitSource'
import { UserRegistrationResponseBody } from '@standardnotes/api'
import { HttpError, HttpResponse, SignInResponse } from '@standardnotes/responses'
import { HttpResponse, SignInResponse } from '@standardnotes/responses'
import { AbstractService } from '../Service/AbstractService'
export type CredentialsChangeFunctionResponse = { error?: HttpError }
export enum AccountEvent {
SignedInOrRegistered = 'SignedInOrRegistered',
SignedOut = 'SignedOut',
}
export interface SignedInOrRegisteredEventPayload {
ephemeral: boolean
mergeLocal: boolean
awaitSync: boolean
checkIntegrity: boolean
}
export interface SignedOutEventPayload {
source: DeinitSource
}
export interface AccountEventData {
payload: Either<SignedInOrRegisteredEventPayload, SignedOutEventPayload>
}
import { AccountEventData } from './AccountEventData'
import { AccountEvent } from './AccountEvent'
export interface UserClientInterface extends AbstractService<AccountEvent, AccountEventData> {
getUserUuid(): string
isSignedIn(): boolean
register(
email: string,

View File

@@ -10,13 +10,6 @@ import {
import { KeyParamsOrigination, UserRequestType } from '@standardnotes/common'
import { UuidGenerator } from '@standardnotes/utils'
import { UserApiServiceInterface, UserRegistrationResponseBody } from '@standardnotes/api'
import {
AccountEventData,
AccountEvent,
SignedInOrRegisteredEventPayload,
CredentialsChangeFunctionResponse,
} from '@standardnotes/services'
import * as Messages from '../Strings/Messages'
import { InfoStrings } from '../Strings/InfoStrings'
import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
@@ -39,6 +32,10 @@ import { SessionsClientInterface } from '../Session/SessionsClientInterface'
import { ProtectionsClientInterface } from '../Protection/ProtectionClientInterface'
import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface'
import { InternalEventInterface } from '../Internal/InternalEventInterface'
import { AccountEventData } from './AccountEventData'
import { AccountEvent } from './AccountEvent'
import { SignedInOrRegisteredEventPayload } from './SignedInOrRegisteredEventPayload'
import { CredentialsChangeFunctionResponse } from './CredentialsChangeFunctionResponse'
export class UserService
extends AbstractService<AccountEvent, AccountEventData>
@@ -115,6 +112,10 @@ export class UserService
;(this.userApiService as unknown) = undefined
}
getUserUuid(): string {
return this.sessionManager.userUuid
}
isSignedIn(): boolean {
return this.sessionManager.isSignedIn()
}

View File

@@ -1,5 +1,10 @@
export * from './Alert/AlertService'
export * from './Api/ApiServiceInterface'
export * from './Api/ApiServiceEventData'
export * from './Api/ApiServiceEvent'
export * from './Api/MetaReceivedData'
export * from './Api/SessionRefreshedData'
export * from './Application/AppGroupManagedApplication'
export * from './Application/ApplicationInterface'
@@ -24,6 +29,7 @@ export * from './Challenge'
export * from './Component/ComponentManagerInterface'
export * from './Component/ComponentViewerError'
export * from './Component/ComponentViewerInterface'
export * from './Component/ComponentViewerItem'
export * from './Contacts/ContactServiceInterface'
export * from './Contacts/ContactService'
@@ -69,6 +75,7 @@ export * from './Event/EventObserver'
export * from './Event/SyncEvent'
export * from './Event/SyncEventReceiver'
export * from './Event/WebAppEvent'
export * from './Event/ApplicationStageChangedEventPayload'
export * from './Feature/FeaturesClientInterface'
export * from './Feature/FeaturesEvent'
@@ -140,8 +147,9 @@ export * from './Strings/Messages'
export * from './Subscription/AppleIAPProductId'
export * from './Subscription/AppleIAPReceipt'
export * from './Subscription/SubscriptionClientInterface'
export * from './Subscription/SubscriptionManagerInterface'
export * from './Subscription/SubscriptionManager'
export * from './Subscription/SubscriptionManagerEvent'
export * from './Sync/SyncMode'
export * from './Sync/SyncOptions'
@@ -152,6 +160,11 @@ export * from './Sync/SyncSource'
export * from './User/UserClientInterface'
export * from './User/UserClientInterface'
export * from './User/UserService'
export * from './User/AccountEvent'
export * from './User/AccountEventData'
export * from './User/CredentialsChangeFunctionResponse'
export * from './User/SignedInOrRegisteredEventPayload'
export * from './User/SignedOutEventPayload'
export * from './UserEvent/UserEventService'
export * from './UserEvent/UserEventServiceEvent'

View File

@@ -29,7 +29,6 @@ import * as Models from '@standardnotes/models'
import * as Responses from '@standardnotes/responses'
import * as InternalServices from '../Services'
import * as Utils from '@standardnotes/utils'
import { Subscription } from '@standardnotes/security'
import { UuidString, ApplicationEventPayload } from '../Types'
import { applicationEventForSyncEvent } from '@Lib/Application/Event'
import {
@@ -50,7 +49,7 @@ import {
EncryptionServiceEvent,
FilesBackupService,
FileService,
SubscriptionClientInterface,
SubscriptionManagerInterface,
SubscriptionManager,
ChallengePrompt,
Challenge,
@@ -151,7 +150,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
private declare userRequestServer: UserRequestServerInterface
private declare subscriptionApiService: SubscriptionApiServiceInterface
private declare subscriptionServer: SubscriptionServerInterface
private declare subscriptionManager: SubscriptionClientInterface
private declare subscriptionManager: SubscriptionManagerInterface
private declare webSocketApiService: WebSocketApiServiceInterface
private declare webSocketServer: WebSocketServerInterface
@@ -275,7 +274,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
this.defineInternalEventHandlers()
}
get subscriptions(): ExternalServices.SubscriptionClientInterface {
get subscriptions(): ExternalServices.SubscriptionManagerInterface {
return this.subscriptionManager
}
@@ -407,6 +406,10 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
return this.sharedVaultService
}
public get preferences(): ExternalServices.PreferenceServiceInterface {
return this.preferencesService
}
public computePrivateUsername(username: string): Promise<string | undefined> {
return ComputePrivateUsername(this.options.crypto, username)
}
@@ -633,6 +636,11 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
}
}
this.internalEventBus.publish({
type: event,
payload: data,
})
void this.migrationService.handleApplicationEvent(event)
}
@@ -671,19 +679,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
return Common.compareVersions(userVersion, Common.ProtocolVersion.V004) >= 0
}
public async getUserSubscription(): Promise<Subscription | Responses.ClientDisplayableError | undefined> {
return this.sessionManager.getSubscription()
}
public async getAvailableSubscriptions(): Promise<
Responses.AvailableSubscriptions | Responses.ClientDisplayableError
> {
if (this.isThirdPartyHostUsed()) {
return ClientDisplayableError.FromString('Third party hosts do not support subscriptions.')
}
return this.sessionManager.getAvailableSubscriptions()
}
/**
* Begin streaming items to display in the UI. The stream callback will be called
* immediately with the present items that match the constraint, and over time whenever
@@ -1268,9 +1263,9 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
this.createSubscriptionApiService()
this.createWebSocketServer()
this.createWebSocketApiService()
this.createSubscriptionManager()
this.createWebSocketsService()
this.createSessionManager()
this.createSubscriptionManager()
this.createHistoryManager()
this.createSyncManager()
this.createProtectionService()
@@ -1498,9 +1493,10 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
private createFeaturesService() {
this.featuresService = new InternalServices.SNFeaturesService(
this.diskStorageService,
this.apiService,
this.itemManager,
this.mutator,
this.subscriptions,
this.apiService,
this.webSocketsService,
this.settingsService,
this.userService,
@@ -1517,8 +1513,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
void this.notifyEvent(ApplicationEvent.UserRolesChanged)
break
}
case ExternalServices.FeaturesEvent.FeaturesUpdated: {
void this.notifyEvent(ApplicationEvent.FeaturesUpdated)
case ExternalServices.FeaturesEvent.FeaturesAvailabilityChanged: {
void this.notifyEvent(ApplicationEvent.FeaturesAvailabilityChanged)
break
}
case ExternalServices.FeaturesEvent.DidPurchaseSubscription: {
@@ -1561,6 +1557,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
internalEventBus: this.internalEventBus,
legacySessionStorageMapper: this.legacySessionStorageMapper,
backups: this.fileBackups,
preferences: this.preferencesService,
})
this.services.push(this.migrationService)
}
@@ -1629,7 +1626,13 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
}
private createSubscriptionManager() {
this.subscriptionManager = new SubscriptionManager(this.subscriptionApiService, this.internalEventBus)
this.subscriptionManager = new SubscriptionManager(
this.subscriptionApiService,
this.sessions,
this.storage,
this.internalEventBus,
)
this.services.push(this.subscriptionManager)
}
private createItemManager() {
@@ -1647,8 +1650,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
this.alertService,
this.environment,
this.platform,
this.internalEventBus,
this.deviceInterface,
this.internalEventBus,
)
this.services.push(this.componentManagerService)
}

View File

@@ -5,6 +5,7 @@ import {
InternalEventBusInterface,
EncryptionService,
MutatorClientInterface,
PreferenceServiceInterface,
} from '@standardnotes/services'
import { SNSessionManager } from '../Services/Session/SessionManager'
import { ApplicationIdentifier } from '@standardnotes/common'
@@ -23,6 +24,7 @@ export type MigrationServices = {
mutator: MutatorClientInterface
singletonManager: SNSingletonManager
featuresService: SNFeaturesService
preferences: PreferenceServiceInterface
environment: Environment
platform: Platform
identifier: ApplicationIdentifier

Some files were not shown because too many files have changed in this diff Show More