refactor: native feature management (#2350)
This commit is contained in:
@@ -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": "*",
|
||||
|
||||
@@ -4,4 +4,6 @@ export enum SubscriptionApiOperations {
|
||||
ListingInvites,
|
||||
AcceptingInvite,
|
||||
ConfirmAppleIAP,
|
||||
GetSubscription,
|
||||
GetAvailableSubscriptions,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export type GetUserSubscriptionRequestParams = {
|
||||
userUuid: string
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>>
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
7
packages/features/src/Domain/Feature/RoleFields.ts
Normal file
7
packages/features/src/Domain/Feature/RoleFields.ts
Normal 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[]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ComponentFeatureDescription } from './ComponentFeatureDescription'
|
||||
|
||||
export type ThirdPartyFeatureDescription = ComponentFeatureDescription & {
|
||||
url: string
|
||||
}
|
||||
28
packages/features/src/Domain/Feature/TypeGuards.ts
Normal file
28
packages/features/src/Domain/Feature/TypeGuards.ts
Normal 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
|
||||
)
|
||||
}
|
||||
10
packages/features/src/Domain/Feature/UIFeatureDescription.ts
Normal file
10
packages/features/src/Domain/Feature/UIFeatureDescription.ts
Normal 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
|
||||
@@ -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],
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FeatureDescription } from '../Feature/FeatureDescription'
|
||||
import { AnyFeatureDescription } from '../Feature/AnyFeatureDescription'
|
||||
|
||||
export function experimentalFeatures(): FeatureDescription[] {
|
||||
export function experimentalFeatures(): AnyFeatureDescription[] {
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
32
packages/features/src/Domain/Lists/NativeEditors.ts
Normal file
32
packages/features/src/Domain/Lists/NativeEditors.ts
Normal 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,
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -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],
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('component model', () => {
|
||||
package_info: {
|
||||
note_type: NoteType.Authentication,
|
||||
},
|
||||
} as ComponentContent),
|
||||
} as unknown as ComponentContent),
|
||||
...PayloadTimestampDefaults(),
|
||||
},
|
||||
PayloadSource.Constructor,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
export * from './Component'
|
||||
export * from './ComponentMutator'
|
||||
export * from './ComponentContent'
|
||||
export * from './ComponentInterface'
|
||||
export * from './ComponentOrNativeFeature'
|
||||
export * from './PackageInfo'
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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> {}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './Theme'
|
||||
export * from './ThemeMutator'
|
||||
export * from './ThemeInterface'
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,7 @@
|
||||
export enum EditorFontSize {
|
||||
ExtraSmall = 'ExtraSmall',
|
||||
Small = 'Small',
|
||||
Normal = 'Normal',
|
||||
Medium = 'Medium',
|
||||
Large = 'Large',
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export enum EditorLineHeight {
|
||||
None = 'None',
|
||||
Tight = 'Tight',
|
||||
Snug = 'Snug',
|
||||
Normal = 'Normal',
|
||||
Relaxed = 'Relaxed',
|
||||
Loose = 'Loose',
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export enum EditorLineWidth {
|
||||
Narrow = 'Narrow',
|
||||
Wide = 'Wide',
|
||||
Dynamic = 'Dynamic',
|
||||
FullWidth = 'FullWidth',
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export enum NewNoteTitleFormat {
|
||||
CurrentDateAndTime = 'CurrentDateAndTime',
|
||||
CurrentNoteCount = 'CurrentNoteCount',
|
||||
CustomFormat = 'CustomFormat',
|
||||
Empty = 'Empty',
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FeatureDescription } from '@standardnotes/features'
|
||||
import { AnyFeatureDescription } from '@standardnotes/features'
|
||||
|
||||
export type GetOfflineFeaturesResponse = {
|
||||
features: FeatureDescription[]
|
||||
features: AnyFeatureDescription[]
|
||||
roles: string[]
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { FeatureDescription } from '@standardnotes/features'
|
||||
|
||||
export type UserFeaturesData = {
|
||||
features: FeatureDescription[]
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import { UserFeaturesData } from './UserFeaturesData'
|
||||
|
||||
export type UserFeaturesResponse = UserFeaturesData
|
||||
@@ -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'
|
||||
|
||||
6
packages/services/src/Domain/Api/ApiServiceEvent.ts
Normal file
6
packages/services/src/Domain/Api/ApiServiceEvent.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/* istanbul ignore file */
|
||||
|
||||
export enum ApiServiceEvent {
|
||||
MetaReceived = 'MetaReceived',
|
||||
SessionRefreshed = 'SessionRefreshed',
|
||||
}
|
||||
5
packages/services/src/Domain/Api/ApiServiceEventData.ts
Normal file
5
packages/services/src/Domain/Api/ApiServiceEventData.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Either } from '@standardnotes/common'
|
||||
import { SessionRefreshedData } from './SessionRefreshedData'
|
||||
import { MetaReceivedData } from './MetaReceivedData'
|
||||
|
||||
export type ApiServiceEventData = Either<MetaReceivedData, SessionRefreshedData>
|
||||
@@ -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 {}
|
||||
|
||||
6
packages/services/src/Domain/Api/MetaReceivedData.ts
Normal file
6
packages/services/src/Domain/Api/MetaReceivedData.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Role } from '@standardnotes/security'
|
||||
|
||||
export type MetaReceivedData = {
|
||||
userUuid: string
|
||||
userRoles: Role[]
|
||||
}
|
||||
5
packages/services/src/Domain/Api/SessionRefreshedData.ts
Normal file
5
packages/services/src/Domain/Api/SessionRefreshedData.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Session } from '@standardnotes/domain-core'
|
||||
|
||||
export type SessionRefreshedData = {
|
||||
session: Session
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ApplicationStage } from './../Application/ApplicationStage'
|
||||
|
||||
export type ApplicationStageChangedEventPayload = {
|
||||
stage: ApplicationStage
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export enum FeaturesEvent {
|
||||
UserRolesChanged = 'UserRolesChanged',
|
||||
FeaturesUpdated = 'FeaturesUpdated',
|
||||
FeaturesAvailabilityChanged = 'Features:FeaturesAvailabilityChanged',
|
||||
DidPurchaseSubscription = 'DidPurchaseSubscription',
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
|
||||
export type SetOfflineFeaturesFunctionResponse = ClientDisplayableError | undefined
|
||||
export type SetOfflineFeaturesFunctionResponse = ClientDisplayableError | void
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }>
|
||||
}
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum SubscriptionManagerEvent {
|
||||
DidFetchSubscription = 'Subscription:DidFetchSubscription',
|
||||
}
|
||||
@@ -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 }>
|
||||
}
|
||||
4
packages/services/src/Domain/User/AccountEvent.ts
Normal file
4
packages/services/src/Domain/User/AccountEvent.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export enum AccountEvent {
|
||||
SignedInOrRegistered = 'SignedInOrRegistered',
|
||||
SignedOut = 'SignedOut',
|
||||
}
|
||||
7
packages/services/src/Domain/User/AccountEventData.ts
Normal file
7
packages/services/src/Domain/User/AccountEventData.ts
Normal 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>
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { HttpError } from '@standardnotes/responses'
|
||||
|
||||
export type CredentialsChangeFunctionResponse = { error?: HttpError }
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface SignedInOrRegisteredEventPayload {
|
||||
ephemeral: boolean
|
||||
mergeLocal: boolean
|
||||
awaitSync: boolean
|
||||
checkIntegrity: boolean
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { DeinitSource } from '../Application/DeinitSource'
|
||||
|
||||
export interface SignedOutEventPayload {
|
||||
source: DeinitSource
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user