chore: remove calling payments server for subscriptions if using third party api hosts (#2398)
This commit is contained in:
@@ -9,13 +9,13 @@ import { AnyFeatureDescription } from '@standardnotes/features'
|
|||||||
export interface LegacyApiServiceInterface
|
export interface LegacyApiServiceInterface
|
||||||
extends AbstractService<ApiServiceEvent, ApiServiceEventData>,
|
extends AbstractService<ApiServiceEvent, ApiServiceEventData>,
|
||||||
FilesApiInterface {
|
FilesApiInterface {
|
||||||
isThirdPartyHostUsed(): boolean
|
|
||||||
setHost(host: string): Promise<void>
|
setHost(host: string): Promise<void>
|
||||||
getHost(): string
|
getHost(): string
|
||||||
|
|
||||||
downloadOfflineFeaturesFromRepo(
|
downloadOfflineFeaturesFromRepo(dto: {
|
||||||
repo: SNFeatureRepo,
|
repo: SNFeatureRepo
|
||||||
): Promise<{ features: AnyFeatureDescription[]; roles: string[] } | ClientDisplayableError>
|
trustedFeatureHosts: string[]
|
||||||
|
}): Promise<{ features: AnyFeatureDescription[]; roles: string[] } | ClientDisplayableError>
|
||||||
|
|
||||||
downloadFeatureUrl(url: string): Promise<HttpResponse>
|
downloadFeatureUrl(url: string): Promise<HttpResponse>
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,6 @@ export interface ApplicationInterface {
|
|||||||
|
|
||||||
hasAccount(): boolean
|
hasAccount(): boolean
|
||||||
setCustomHost(host: string): Promise<void>
|
setCustomHost(host: string): Promise<void>
|
||||||
isThirdPartyHostUsed(): boolean
|
|
||||||
isUsingHomeServer(): Promise<boolean>
|
isUsingHomeServer(): Promise<boolean>
|
||||||
|
|
||||||
importData(data: BackupFile, awaitSync?: boolean): Promise<ImportDataReturnType>
|
importData(data: BackupFile, awaitSync?: boolean): Promise<ImportDataReturnType>
|
||||||
|
|||||||
@@ -7,14 +7,17 @@ import { SubscriptionApiServiceInterface } from '@standardnotes/api'
|
|||||||
import { Invitation } from '@standardnotes/models'
|
import { Invitation } from '@standardnotes/models'
|
||||||
import { InternalEventBusInterface } from '..'
|
import { InternalEventBusInterface } from '..'
|
||||||
import { SubscriptionManager } from './SubscriptionManager'
|
import { SubscriptionManager } from './SubscriptionManager'
|
||||||
|
import { IsApplicationUsingThirdPartyHost } from '../UseCase/IsApplicationUsingThirdPartyHost'
|
||||||
|
import { Result } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
describe('SubscriptionManager', () => {
|
describe('SubscriptionManager', () => {
|
||||||
let subscriptionApiService: SubscriptionApiServiceInterface
|
let subscriptionApiService: SubscriptionApiServiceInterface
|
||||||
let internalEventBus: InternalEventBusInterface
|
let internalEventBus: InternalEventBusInterface
|
||||||
let sessions: SessionsClientInterface
|
let sessions: SessionsClientInterface
|
||||||
let storage: StorageServiceInterface
|
let storage: StorageServiceInterface
|
||||||
|
let isApplicationUsingThirdPartyHostUseCase: IsApplicationUsingThirdPartyHost
|
||||||
|
|
||||||
const createManager = () => new SubscriptionManager(subscriptionApiService, sessions, storage, internalEventBus)
|
const createManager = () => new SubscriptionManager(subscriptionApiService, sessions, storage, isApplicationUsingThirdPartyHostUseCase, internalEventBus)
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
subscriptionApiService = {} as jest.Mocked<SubscriptionApiServiceInterface>
|
subscriptionApiService = {} as jest.Mocked<SubscriptionApiServiceInterface>
|
||||||
@@ -31,6 +34,9 @@ describe('SubscriptionManager', () => {
|
|||||||
internalEventBus = {} as jest.Mocked<InternalEventBusInterface>
|
internalEventBus = {} as jest.Mocked<InternalEventBusInterface>
|
||||||
internalEventBus.addEventHandler = jest.fn()
|
internalEventBus.addEventHandler = jest.fn()
|
||||||
internalEventBus.publish = jest.fn()
|
internalEventBus.publish = jest.fn()
|
||||||
|
|
||||||
|
isApplicationUsingThirdPartyHostUseCase = {} as jest.Mocked<IsApplicationUsingThirdPartyHost>
|
||||||
|
isApplicationUsingThirdPartyHostUseCase.execute = jest.fn().mockReturnValue(Result.ok(false))
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('event handling', () => {
|
describe('event handling', () => {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
} from '@standardnotes/responses'
|
} from '@standardnotes/responses'
|
||||||
import { SubscriptionManagerEvent } from './SubscriptionManagerEvent'
|
import { SubscriptionManagerEvent } from './SubscriptionManagerEvent'
|
||||||
import { ApplicationStageChangedEventPayload } from '../Event/ApplicationStageChangedEventPayload'
|
import { ApplicationStageChangedEventPayload } from '../Event/ApplicationStageChangedEventPayload'
|
||||||
|
import { IsApplicationUsingThirdPartyHost } from '../UseCase/IsApplicationUsingThirdPartyHost'
|
||||||
|
|
||||||
export class SubscriptionManager
|
export class SubscriptionManager
|
||||||
extends AbstractService<SubscriptionManagerEvent>
|
extends AbstractService<SubscriptionManagerEvent>
|
||||||
@@ -34,6 +35,7 @@ export class SubscriptionManager
|
|||||||
private subscriptionApiService: SubscriptionApiServiceInterface,
|
private subscriptionApiService: SubscriptionApiServiceInterface,
|
||||||
private sessions: SessionsClientInterface,
|
private sessions: SessionsClientInterface,
|
||||||
private storage: StorageServiceInterface,
|
private storage: StorageServiceInterface,
|
||||||
|
private isApplicationUsingThirdPartyHostUseCase: IsApplicationUsingThirdPartyHost,
|
||||||
protected override internalEventBus: InternalEventBusInterface,
|
protected override internalEventBus: InternalEventBusInterface,
|
||||||
) {
|
) {
|
||||||
super(internalEventBus)
|
super(internalEventBus)
|
||||||
@@ -43,7 +45,15 @@ export class SubscriptionManager
|
|||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case ApplicationEvent.Launched: {
|
case ApplicationEvent.Launched: {
|
||||||
void this.fetchOnlineSubscription()
|
void this.fetchOnlineSubscription()
|
||||||
void this.fetchAvailableSubscriptions()
|
|
||||||
|
const isThirdPartyHostUsedOrError = this.isApplicationUsingThirdPartyHostUseCase.execute()
|
||||||
|
if (isThirdPartyHostUsedOrError.isFailed()) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
const isThirdPartyHostUsed = isThirdPartyHostUsedOrError.getValue()
|
||||||
|
if (!isThirdPartyHostUsed) {
|
||||||
|
void this.fetchAvailableSubscriptions()
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { Result } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
|
import { GetHost } from '../..'
|
||||||
|
import { IsApplicationUsingThirdPartyHost } from './IsApplicationUsingThirdPartyHost'
|
||||||
|
|
||||||
|
describe('IsApplicationUsingThirdPartyHost', () => {
|
||||||
|
let getHostUseCase: GetHost
|
||||||
|
|
||||||
|
const createUseCase = () => new IsApplicationUsingThirdPartyHost(getHostUseCase)
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
getHostUseCase = {} as jest.Mocked<GetHost>
|
||||||
|
getHostUseCase.execute = jest.fn().mockReturnValue(Result.ok('https://api.standardnotes.com'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns true if host is localhost', () => {
|
||||||
|
getHostUseCase.execute = jest.fn().mockReturnValue(Result.ok('http://localhost:3000'))
|
||||||
|
|
||||||
|
const useCase = createUseCase()
|
||||||
|
const result = useCase.execute()
|
||||||
|
|
||||||
|
expect(result.getValue()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns false if host is api.standardnotes.com', () => {
|
||||||
|
getHostUseCase.execute = jest.fn().mockReturnValue(Result.ok('https://api.standardnotes.com'))
|
||||||
|
|
||||||
|
const useCase = createUseCase()
|
||||||
|
const result = useCase.execute()
|
||||||
|
|
||||||
|
expect(result.getValue()).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns false if host is sync.standardnotes.org', () => {
|
||||||
|
getHostUseCase.execute = jest.fn().mockReturnValue(Result.ok('https://sync.standardnotes.org'))
|
||||||
|
|
||||||
|
const useCase = createUseCase()
|
||||||
|
const result = useCase.execute()
|
||||||
|
|
||||||
|
expect(result.getValue()).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns false if host is files.standardnotes.com', () => {
|
||||||
|
getHostUseCase.execute = jest.fn().mockReturnValue(Result.ok('https://files.standardnotes.com'))
|
||||||
|
|
||||||
|
const useCase = createUseCase()
|
||||||
|
const result = useCase.execute()
|
||||||
|
|
||||||
|
expect(result.getValue()).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns true if host is not first party', () => {
|
||||||
|
getHostUseCase.execute = jest.fn().mockReturnValue(Result.ok('https://example.com'))
|
||||||
|
|
||||||
|
const useCase = createUseCase()
|
||||||
|
const result = useCase.execute()
|
||||||
|
|
||||||
|
expect(result.getValue()).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
|
import { GetHost } from './GetHost'
|
||||||
|
|
||||||
|
export class IsApplicationUsingThirdPartyHost implements SyncUseCaseInterface<boolean> {
|
||||||
|
private readonly APPLICATION_DEFAULT_HOSTS = ['api.standardnotes.com', 'sync.standardnotes.org']
|
||||||
|
|
||||||
|
private readonly FILES_DEFAULT_HOSTS = ['files.standardnotes.com']
|
||||||
|
|
||||||
|
constructor(private getHostUseCase: GetHost) {}
|
||||||
|
|
||||||
|
execute(): Result<boolean> {
|
||||||
|
const result = this.getHostUseCase.execute()
|
||||||
|
if (result.isFailed()) {
|
||||||
|
return Result.fail(result.getError())
|
||||||
|
}
|
||||||
|
|
||||||
|
const host = result.getValue()
|
||||||
|
|
||||||
|
return Result.ok(!this.isUrlFirstParty(host))
|
||||||
|
}
|
||||||
|
|
||||||
|
private isUrlFirstParty(url: string): boolean {
|
||||||
|
try {
|
||||||
|
const { host } = new URL(url)
|
||||||
|
return this.APPLICATION_DEFAULT_HOSTS.includes(host) || this.FILES_DEFAULT_HOSTS.includes(host)
|
||||||
|
} catch (error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -180,6 +180,7 @@ export * from './UseCase/ChangeAndSaveItem'
|
|||||||
export * from './UseCase/DiscardItemsLocally'
|
export * from './UseCase/DiscardItemsLocally'
|
||||||
export * from './UseCase/GenerateUuid'
|
export * from './UseCase/GenerateUuid'
|
||||||
export * from './UseCase/GetHost'
|
export * from './UseCase/GetHost'
|
||||||
|
export * from './UseCase/IsApplicationUsingThirdPartyHost'
|
||||||
export * from './UseCase/SetHost'
|
export * from './UseCase/SetHost'
|
||||||
export * from './User/AccountEvent'
|
export * from './User/AccountEvent'
|
||||||
export * from './User/AccountEventData'
|
export * from './User/AccountEventData'
|
||||||
|
|||||||
@@ -962,10 +962,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public isThirdPartyHostUsed(): boolean {
|
|
||||||
return this.legacyApi.isThirdPartyHostUsed()
|
|
||||||
}
|
|
||||||
|
|
||||||
async isUsingHomeServer(): Promise<boolean> {
|
async isUsingHomeServer(): Promise<boolean> {
|
||||||
const homeServerService = this.dependencies.get<HomeServerServiceInterface>(TYPES.HomeServerService)
|
const homeServerService = this.dependencies.get<HomeServerServiceInterface>(TYPES.HomeServerService)
|
||||||
|
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ import {
|
|||||||
GenerateUuid,
|
GenerateUuid,
|
||||||
GetVaultItems,
|
GetVaultItems,
|
||||||
ValidateVaultPassword,
|
ValidateVaultPassword,
|
||||||
|
IsApplicationUsingThirdPartyHost,
|
||||||
} from '@standardnotes/services'
|
} from '@standardnotes/services'
|
||||||
import { ItemManager } from '../../Services/Items/ItemManager'
|
import { ItemManager } from '../../Services/Items/ItemManager'
|
||||||
import { PayloadManager } from '../../Services/Payloads/PayloadManager'
|
import { PayloadManager } from '../../Services/Payloads/PayloadManager'
|
||||||
@@ -243,6 +244,10 @@ export class Dependencies {
|
|||||||
return new GetHost(this.get<LegacyApiService>(TYPES.LegacyApiService))
|
return new GetHost(this.get<LegacyApiService>(TYPES.LegacyApiService))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.factory.set(TYPES.IsApplicationUsingThirdPartyHost, () => {
|
||||||
|
return new IsApplicationUsingThirdPartyHost(this.get<GetHost>(TYPES.GetHost))
|
||||||
|
})
|
||||||
|
|
||||||
this.factory.set(TYPES.SetHost, () => {
|
this.factory.set(TYPES.SetHost, () => {
|
||||||
return new SetHost(this.get<HttpService>(TYPES.HttpService), this.get<LegacyApiService>(TYPES.LegacyApiService))
|
return new SetHost(this.get<HttpService>(TYPES.HttpService), this.get<LegacyApiService>(TYPES.LegacyApiService))
|
||||||
})
|
})
|
||||||
@@ -1159,6 +1164,7 @@ export class Dependencies {
|
|||||||
this.get<SessionManager>(TYPES.SessionManager),
|
this.get<SessionManager>(TYPES.SessionManager),
|
||||||
this.get<PureCryptoInterface>(TYPES.Crypto),
|
this.get<PureCryptoInterface>(TYPES.Crypto),
|
||||||
this.get<Logger>(TYPES.Logger),
|
this.get<Logger>(TYPES.Logger),
|
||||||
|
this.get<IsApplicationUsingThirdPartyHost>(TYPES.IsApplicationUsingThirdPartyHost),
|
||||||
this.get<InternalEventBus>(TYPES.InternalEventBus),
|
this.get<InternalEventBus>(TYPES.InternalEventBus),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -1267,6 +1273,7 @@ export class Dependencies {
|
|||||||
this.get<SubscriptionApiService>(TYPES.SubscriptionApiService),
|
this.get<SubscriptionApiService>(TYPES.SubscriptionApiService),
|
||||||
this.get<SessionManager>(TYPES.SessionManager),
|
this.get<SessionManager>(TYPES.SessionManager),
|
||||||
this.get<DiskStorageService>(TYPES.DiskStorageService),
|
this.get<DiskStorageService>(TYPES.DiskStorageService),
|
||||||
|
this.get<IsApplicationUsingThirdPartyHost>(TYPES.IsApplicationUsingThirdPartyHost),
|
||||||
this.get<InternalEventBus>(TYPES.InternalEventBus),
|
this.get<InternalEventBus>(TYPES.InternalEventBus),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -1286,6 +1293,7 @@ export class Dependencies {
|
|||||||
this.get<LegacySessionStorageMapper>(TYPES.LegacySessionStorageMapper),
|
this.get<LegacySessionStorageMapper>(TYPES.LegacySessionStorageMapper),
|
||||||
this.options.identifier,
|
this.options.identifier,
|
||||||
this.get<GetKeyPairs>(TYPES.GetKeyPairs),
|
this.get<GetKeyPairs>(TYPES.GetKeyPairs),
|
||||||
|
this.get<IsApplicationUsingThirdPartyHost>(TYPES.IsApplicationUsingThirdPartyHost),
|
||||||
this.get<InternalEventBus>(TYPES.InternalEventBus),
|
this.get<InternalEventBus>(TYPES.InternalEventBus),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ export const TYPES = {
|
|||||||
ChangeVaultStorageMode: Symbol.for('ChangeVaultStorageMode'),
|
ChangeVaultStorageMode: Symbol.for('ChangeVaultStorageMode'),
|
||||||
ChangeAndSaveItem: Symbol.for('ChangeAndSaveItem'),
|
ChangeAndSaveItem: Symbol.for('ChangeAndSaveItem'),
|
||||||
GetHost: Symbol.for('GetHost'),
|
GetHost: Symbol.for('GetHost'),
|
||||||
|
IsApplicationUsingThirdPartyHost: Symbol.for('IsApplicationUsingThirdPartyHost'),
|
||||||
SetHost: Symbol.for('SetHost'),
|
SetHost: Symbol.for('SetHost'),
|
||||||
GenerateUuid: Symbol.for('GenerateUuid'),
|
GenerateUuid: Symbol.for('GenerateUuid'),
|
||||||
GetVaultItems: Symbol.for('GetVaultItems'),
|
GetVaultItems: Symbol.for('GetVaultItems'),
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
export const APPLICATION_DEFAULT_HOSTS = ['api.standardnotes.com', 'sync.standardnotes.org']
|
|
||||||
|
|
||||||
export const FILES_DEFAULT_HOSTS = ['files.standardnotes.com']
|
|
||||||
|
|
||||||
export const TRUSTED_FEATURE_HOSTS = [
|
|
||||||
'api.standardnotes.com',
|
|
||||||
'extensions.standardnotes.com',
|
|
||||||
'extensions.standardnotes.org',
|
|
||||||
'features.standardnotes.com',
|
|
||||||
'localhost',
|
|
||||||
]
|
|
||||||
|
|
||||||
export enum ExtensionsServerURL {
|
|
||||||
Prod = 'https://extensions.standardnotes.org',
|
|
||||||
}
|
|
||||||
|
|
||||||
const LocalHost = 'localhost'
|
|
||||||
|
|
||||||
export function isUrlFirstParty(url: string): boolean {
|
|
||||||
try {
|
|
||||||
const { host } = new URL(url)
|
|
||||||
return host.startsWith(LocalHost) || APPLICATION_DEFAULT_HOSTS.includes(host) || FILES_DEFAULT_HOSTS.includes(host)
|
|
||||||
} catch (_err) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PROD_OFFLINE_FEATURES_URL = 'https://api.standardnotes.com/v1/offline/features'
|
|
||||||
|
|
||||||
export const LEGACY_PROD_EXT_ORIGIN = 'https://extensions.standardnotes.org'
|
|
||||||
|
|
||||||
export const TRUSTED_CUSTOM_EXTENSIONS_HOSTS = ['listed.to']
|
|
||||||
@@ -73,7 +73,6 @@ import { LegacySession, MapperInterface, Session, SessionToken } from '@standard
|
|||||||
import { HttpServiceInterface } from '@standardnotes/api'
|
import { HttpServiceInterface } from '@standardnotes/api'
|
||||||
import { SNRootKeyParams } from '@standardnotes/encryption'
|
import { SNRootKeyParams } from '@standardnotes/encryption'
|
||||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||||
import { isUrlFirstParty, TRUSTED_FEATURE_HOSTS } from '@Lib/Hosts'
|
|
||||||
import { Paths } from './Paths'
|
import { Paths } from './Paths'
|
||||||
import { DiskStorageService } from '../Storage/DiskStorageService'
|
import { DiskStorageService } from '../Storage/DiskStorageService'
|
||||||
import { UuidString } from '../../Types/UuidString'
|
import { UuidString } from '../../Types/UuidString'
|
||||||
@@ -157,11 +156,6 @@ export class LegacyApiService
|
|||||||
return this.host
|
return this.host
|
||||||
}
|
}
|
||||||
|
|
||||||
public isThirdPartyHostUsed(): boolean {
|
|
||||||
const applicationHost = this.getHost() || ''
|
|
||||||
return !isUrlFirstParty(applicationHost)
|
|
||||||
}
|
|
||||||
|
|
||||||
public getFilesHost(): string {
|
public getFilesHost(): string {
|
||||||
if (!this.filesHost) {
|
if (!this.filesHost) {
|
||||||
throw Error('Attempting to access undefined filesHost')
|
throw Error('Attempting to access undefined filesHost')
|
||||||
@@ -620,19 +614,20 @@ export class LegacyApiService
|
|||||||
return response.data.token
|
return response.data.token
|
||||||
}
|
}
|
||||||
|
|
||||||
public async downloadOfflineFeaturesFromRepo(
|
public async downloadOfflineFeaturesFromRepo(dto: {
|
||||||
repo: SNFeatureRepo,
|
repo: SNFeatureRepo
|
||||||
): Promise<{ features: AnyFeatureDescription[]; roles: string[] } | ClientDisplayableError> {
|
trustedFeatureHosts: string[]
|
||||||
|
}): Promise<{ features: AnyFeatureDescription[]; roles: string[] } | ClientDisplayableError> {
|
||||||
try {
|
try {
|
||||||
const featuresUrl = repo.offlineFeaturesUrl
|
const featuresUrl = dto.repo.offlineFeaturesUrl
|
||||||
const extensionKey = repo.offlineKey
|
const extensionKey = dto.repo.offlineKey
|
||||||
if (!featuresUrl || !extensionKey) {
|
if (!featuresUrl || !extensionKey) {
|
||||||
throw Error('Cannot download offline repo without url and offlineKEy')
|
throw Error('Cannot download offline repo without url and offlineKEy')
|
||||||
}
|
}
|
||||||
|
|
||||||
const { hostname } = new URL(featuresUrl)
|
const { hostname } = new URL(featuresUrl)
|
||||||
|
|
||||||
if (!TRUSTED_FEATURE_HOSTS.includes(hostname)) {
|
if (!dto.trustedFeatureHosts.includes(hostname)) {
|
||||||
return new ClientDisplayableError(`The offline features host ${hostname} is not in the trusted allowlist.`)
|
return new ClientDisplayableError(`The offline features host ${hostname} is not in the trusted allowlist.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { ItemInterface, SNFeatureRepo } from '@standardnotes/models'
|
|||||||
import { SyncService } from '../Sync/SyncService'
|
import { SyncService } from '../Sync/SyncService'
|
||||||
import { SettingName } from '@standardnotes/settings'
|
import { SettingName } from '@standardnotes/settings'
|
||||||
import { FeaturesService } from '@Lib/Services/Features'
|
import { FeaturesService } from '@Lib/Services/Features'
|
||||||
import { RoleName, ContentType, Uuid } from '@standardnotes/domain-core'
|
import { RoleName, ContentType, Uuid, Result } from '@standardnotes/domain-core'
|
||||||
import { NativeFeatureIdentifier, GetFeatures } from '@standardnotes/features'
|
import { NativeFeatureIdentifier, GetFeatures } from '@standardnotes/features'
|
||||||
import { WebSocketsService } from '../Api/WebsocketsService'
|
import { WebSocketsService } from '../Api/WebsocketsService'
|
||||||
import { SettingsService } from '../Settings'
|
import { SettingsService } from '../Settings'
|
||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
SyncServiceInterface,
|
SyncServiceInterface,
|
||||||
UserServiceInterface,
|
UserServiceInterface,
|
||||||
UserService,
|
UserService,
|
||||||
|
IsApplicationUsingThirdPartyHost,
|
||||||
} from '@standardnotes/services'
|
} from '@standardnotes/services'
|
||||||
import { LegacyApiService, SessionManager } from '../Api'
|
import { LegacyApiService, SessionManager } from '../Api'
|
||||||
import { ItemManager } from '../Items'
|
import { ItemManager } from '../Items'
|
||||||
@@ -47,6 +48,7 @@ describe('FeaturesService', () => {
|
|||||||
let internalEventBus: InternalEventBusInterface
|
let internalEventBus: InternalEventBusInterface
|
||||||
let featureService: FeaturesService
|
let featureService: FeaturesService
|
||||||
let logger: LoggerInterface
|
let logger: LoggerInterface
|
||||||
|
let isApplicationUsingThirdPartyHostUseCase: IsApplicationUsingThirdPartyHost
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
logger = {} as jest.Mocked<LoggerInterface>
|
logger = {} as jest.Mocked<LoggerInterface>
|
||||||
@@ -62,7 +64,6 @@ describe('FeaturesService', () => {
|
|||||||
|
|
||||||
apiService = {} as jest.Mocked<LegacyApiService>
|
apiService = {} as jest.Mocked<LegacyApiService>
|
||||||
apiService.addEventObserver = jest.fn()
|
apiService.addEventObserver = jest.fn()
|
||||||
apiService.isThirdPartyHostUsed = jest.fn().mockReturnValue(false)
|
|
||||||
|
|
||||||
itemManager = {} as jest.Mocked<ItemManager>
|
itemManager = {} as jest.Mocked<ItemManager>
|
||||||
itemManager.getItems = jest.fn().mockReturnValue(items)
|
itemManager.getItems = jest.fn().mockReturnValue(items)
|
||||||
@@ -107,6 +108,9 @@ describe('FeaturesService', () => {
|
|||||||
internalEventBus.publish = jest.fn()
|
internalEventBus.publish = jest.fn()
|
||||||
internalEventBus.addEventHandler = jest.fn()
|
internalEventBus.addEventHandler = jest.fn()
|
||||||
|
|
||||||
|
isApplicationUsingThirdPartyHostUseCase = {} as jest.Mocked<IsApplicationUsingThirdPartyHost>
|
||||||
|
isApplicationUsingThirdPartyHostUseCase.execute = jest.fn().mockReturnValue(Result.ok(false))
|
||||||
|
|
||||||
featureService = new FeaturesService(
|
featureService = new FeaturesService(
|
||||||
storageService,
|
storageService,
|
||||||
itemManager,
|
itemManager,
|
||||||
@@ -121,6 +125,7 @@ describe('FeaturesService', () => {
|
|||||||
sessionManager,
|
sessionManager,
|
||||||
crypto,
|
crypto,
|
||||||
logger,
|
logger,
|
||||||
|
isApplicationUsingThirdPartyHostUseCase,
|
||||||
internalEventBus,
|
internalEventBus,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -202,6 +207,7 @@ describe('FeaturesService', () => {
|
|||||||
sessionManager,
|
sessionManager,
|
||||||
crypto,
|
crypto,
|
||||||
logger,
|
logger,
|
||||||
|
isApplicationUsingThirdPartyHostUseCase,
|
||||||
internalEventBus,
|
internalEventBus,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,9 @@ import { MigrateFeatureRepoToUserSettingUseCase } from './UseCase/MigrateFeature
|
|||||||
import { arraysEqual, removeFromArray, lastElement, LoggerInterface } from '@standardnotes/utils'
|
import { arraysEqual, removeFromArray, lastElement, LoggerInterface } from '@standardnotes/utils'
|
||||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||||
import { RoleName, ContentType, Uuid } from '@standardnotes/domain-core'
|
import { RoleName, ContentType, Uuid } from '@standardnotes/domain-core'
|
||||||
import { PROD_OFFLINE_FEATURES_URL } from '../../Hosts'
|
|
||||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||||
import { WebSocketsService } from '../Api/WebsocketsService'
|
import { WebSocketsService } from '../Api/WebsocketsService'
|
||||||
import { WebSocketsServiceEvent } from '../Api/WebSocketsServiceEvent'
|
import { WebSocketsServiceEvent } from '../Api/WebSocketsServiceEvent'
|
||||||
import { TRUSTED_CUSTOM_EXTENSIONS_HOSTS, TRUSTED_FEATURE_HOSTS } from '@Lib/Hosts'
|
|
||||||
import { UserRolesChangedEvent } from '@standardnotes/domain-events'
|
import { UserRolesChangedEvent } from '@standardnotes/domain-events'
|
||||||
import { ExperimentalFeatures, FindNativeFeature, NativeFeatureIdentifier } from '@standardnotes/features'
|
import { ExperimentalFeatures, FindNativeFeature, NativeFeatureIdentifier } from '@standardnotes/features'
|
||||||
import {
|
import {
|
||||||
@@ -48,6 +46,7 @@ import {
|
|||||||
SubscriptionManagerEvent,
|
SubscriptionManagerEvent,
|
||||||
ApplicationEvent,
|
ApplicationEvent,
|
||||||
ApplicationStageChangedEventPayload,
|
ApplicationStageChangedEventPayload,
|
||||||
|
IsApplicationUsingThirdPartyHost,
|
||||||
} from '@standardnotes/services'
|
} from '@standardnotes/services'
|
||||||
|
|
||||||
import { DownloadRemoteThirdPartyFeatureUseCase } from './UseCase/DownloadRemoteThirdPartyFeature'
|
import { DownloadRemoteThirdPartyFeatureUseCase } from './UseCase/DownloadRemoteThirdPartyFeature'
|
||||||
@@ -67,6 +66,18 @@ export class FeaturesService
|
|||||||
|
|
||||||
private getFeatureStatusUseCase = new GetFeatureStatusUseCase(this.items)
|
private getFeatureStatusUseCase = new GetFeatureStatusUseCase(this.items)
|
||||||
|
|
||||||
|
private readonly TRUSTED_FEATURE_HOSTS = [
|
||||||
|
'api.standardnotes.com',
|
||||||
|
'extensions.standardnotes.com',
|
||||||
|
'extensions.standardnotes.org',
|
||||||
|
'features.standardnotes.com',
|
||||||
|
'localhost',
|
||||||
|
]
|
||||||
|
|
||||||
|
private readonly TRUSTED_CUSTOM_EXTENSIONS_HOSTS = ['listed.to']
|
||||||
|
|
||||||
|
private readonly PROD_OFFLINE_FEATURES_URL = 'https://api.standardnotes.com/v1/offline/features'
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private storage: StorageServiceInterface,
|
private storage: StorageServiceInterface,
|
||||||
private items: ItemManagerInterface,
|
private items: ItemManagerInterface,
|
||||||
@@ -81,6 +92,7 @@ export class FeaturesService
|
|||||||
private sessions: SessionsClientInterface,
|
private sessions: SessionsClientInterface,
|
||||||
private crypto: PureCryptoInterface,
|
private crypto: PureCryptoInterface,
|
||||||
private logger: LoggerInterface,
|
private logger: LoggerInterface,
|
||||||
|
private isApplicationUsingThirdPartyHostUseCase: IsApplicationUsingThirdPartyHost,
|
||||||
protected override internalEventBus: InternalEventBusInterface,
|
protected override internalEventBus: InternalEventBusInterface,
|
||||||
) {
|
) {
|
||||||
super(internalEventBus)
|
super(internalEventBus)
|
||||||
@@ -128,7 +140,12 @@ export class FeaturesService
|
|||||||
if (eventName === AccountEvent.SignedInOrRegistered) {
|
if (eventName === AccountEvent.SignedInOrRegistered) {
|
||||||
const featureRepos = this.items.getItems(ContentType.TYPES.ExtensionRepo) as SNFeatureRepo[]
|
const featureRepos = this.items.getItems(ContentType.TYPES.ExtensionRepo) as SNFeatureRepo[]
|
||||||
|
|
||||||
if (!this.api.isThirdPartyHostUsed()) {
|
const isThirdPartyHostUsedOrError = this.isApplicationUsingThirdPartyHostUseCase.execute()
|
||||||
|
if (isThirdPartyHostUsedOrError.isFailed()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const isThirdPartyHostUsed = isThirdPartyHostUsedOrError.getValue()
|
||||||
|
if (!isThirdPartyHostUsed) {
|
||||||
void this.migrateFeatureRepoToUserSetting(featureRepos)
|
void this.migrateFeatureRepoToUserSetting(featureRepos)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -285,7 +302,10 @@ export class FeaturesService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async downloadOfflineRoles(repo: SNFeatureRepo): Promise<SetOfflineFeaturesFunctionResponse> {
|
private async downloadOfflineRoles(repo: SNFeatureRepo): Promise<SetOfflineFeaturesFunctionResponse> {
|
||||||
const result = await this.api.downloadOfflineFeaturesFromRepo(repo)
|
const result = await this.api.downloadOfflineFeaturesFromRepo({
|
||||||
|
repo,
|
||||||
|
trustedFeatureHosts: this.TRUSTED_FEATURE_HOSTS,
|
||||||
|
})
|
||||||
|
|
||||||
if (result instanceof ClientDisplayableError) {
|
if (result instanceof ClientDisplayableError) {
|
||||||
return result
|
return result
|
||||||
@@ -301,7 +321,7 @@ export class FeaturesService
|
|||||||
|
|
||||||
public async migrateFeatureRepoToOfflineEntitlements(featureRepos: SNFeatureRepo[] = []): Promise<void> {
|
public async migrateFeatureRepoToOfflineEntitlements(featureRepos: SNFeatureRepo[] = []): Promise<void> {
|
||||||
const usecase = new MigrateFeatureRepoToOfflineEntitlementsUseCase(this.mutator)
|
const usecase = new MigrateFeatureRepoToOfflineEntitlementsUseCase(this.mutator)
|
||||||
const updatedRepos = await usecase.execute(featureRepos)
|
const updatedRepos = await usecase.execute({ featureRepos, prodOfflineFeaturesUrl: this.PROD_OFFLINE_FEATURES_URL })
|
||||||
|
|
||||||
if (updatedRepos.length > 0) {
|
if (updatedRepos.length > 0) {
|
||||||
await this.downloadOfflineRoles(updatedRepos[0])
|
await this.downloadOfflineRoles(updatedRepos[0])
|
||||||
@@ -322,7 +342,7 @@ export class FeaturesService
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasFirstPartyOfflineSubscription = offlineRepo.content.offlineFeaturesUrl === PROD_OFFLINE_FEATURES_URL
|
const hasFirstPartyOfflineSubscription = offlineRepo.content.offlineFeaturesUrl === this.PROD_OFFLINE_FEATURES_URL
|
||||||
return hasFirstPartyOfflineSubscription || new URL(offlineRepo.content.offlineFeaturesUrl).hostname === 'localhost'
|
return hasFirstPartyOfflineSubscription || new URL(offlineRepo.content.offlineFeaturesUrl).hostname === 'localhost'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,7 +447,7 @@ export class FeaturesService
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const trustedCustomExtensionsUrls = [...TRUSTED_FEATURE_HOSTS, ...TRUSTED_CUSTOM_EXTENSIONS_HOSTS]
|
const trustedCustomExtensionsUrls = [...this.TRUSTED_FEATURE_HOSTS, ...this.TRUSTED_CUSTOM_EXTENSIONS_HOSTS]
|
||||||
const { host } = new URL(url)
|
const { host } = new URL(url)
|
||||||
|
|
||||||
const usecase = new DownloadRemoteThirdPartyFeatureUseCase(this.api, this.items, this.alerts)
|
const usecase = new DownloadRemoteThirdPartyFeatureUseCase(this.api, this.items, this.alerts)
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { LEGACY_PROD_EXT_ORIGIN, PROD_OFFLINE_FEATURES_URL } from '@Lib/Hosts'
|
|
||||||
import { SNFeatureRepo } from '@standardnotes/models'
|
import { SNFeatureRepo } from '@standardnotes/models'
|
||||||
import { MutatorClientInterface } from '@standardnotes/services'
|
import { MutatorClientInterface } from '@standardnotes/services'
|
||||||
|
|
||||||
export class MigrateFeatureRepoToOfflineEntitlementsUseCase {
|
export class MigrateFeatureRepoToOfflineEntitlementsUseCase {
|
||||||
|
private readonly LEGACY_PROD_EXT_ORIGIN = 'https://extensions.standardnotes.org'
|
||||||
|
|
||||||
constructor(private mutator: MutatorClientInterface) {}
|
constructor(private mutator: MutatorClientInterface) {}
|
||||||
|
|
||||||
async execute(featureRepos: SNFeatureRepo[] = []): Promise<SNFeatureRepo[]> {
|
async execute(dto: { featureRepos: SNFeatureRepo[]; prodOfflineFeaturesUrl: string }): Promise<SNFeatureRepo[]> {
|
||||||
const updatedRepos: SNFeatureRepo[] = []
|
const updatedRepos: SNFeatureRepo[] = []
|
||||||
for (const item of featureRepos) {
|
for (const item of dto.featureRepos) {
|
||||||
if (item.migratedToOfflineEntitlements) {
|
if (item.migratedToOfflineEntitlements) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -19,7 +20,7 @@ export class MigrateFeatureRepoToOfflineEntitlementsUseCase {
|
|||||||
const repoUrl = item.onlineUrl
|
const repoUrl = item.onlineUrl
|
||||||
const { origin } = new URL(repoUrl)
|
const { origin } = new URL(repoUrl)
|
||||||
|
|
||||||
if (!origin.includes(LEGACY_PROD_EXT_ORIGIN)) {
|
if (!origin.includes(this.LEGACY_PROD_EXT_ORIGIN)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@ export class MigrateFeatureRepoToOfflineEntitlementsUseCase {
|
|||||||
const userKey = userKeyMatch[0]
|
const userKey = userKeyMatch[0]
|
||||||
|
|
||||||
const updatedRepo = await this.mutator.changeFeatureRepo(item, (m) => {
|
const updatedRepo = await this.mutator.changeFeatureRepo(item, (m) => {
|
||||||
m.offlineFeaturesUrl = PROD_OFFLINE_FEATURES_URL
|
m.offlineFeaturesUrl = dto.prodOfflineFeaturesUrl
|
||||||
m.offlineKey = userKey
|
m.offlineKey = userKey
|
||||||
m.migratedToOfflineEntitlements = true
|
m.migratedToOfflineEntitlements = true
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
ApplicationStageChangedEventPayload,
|
ApplicationStageChangedEventPayload,
|
||||||
ApplicationStage,
|
ApplicationStage,
|
||||||
GetKeyPairs,
|
GetKeyPairs,
|
||||||
|
IsApplicationUsingThirdPartyHost,
|
||||||
} from '@standardnotes/services'
|
} from '@standardnotes/services'
|
||||||
import { Base64String, PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
import { Base64String, PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||||
import {
|
import {
|
||||||
@@ -105,6 +106,7 @@ export class SessionManager
|
|||||||
private legacySessionStorageMapper: MapperInterface<LegacySession, Record<string, unknown>>,
|
private legacySessionStorageMapper: MapperInterface<LegacySession, Record<string, unknown>>,
|
||||||
private workspaceIdentifier: string,
|
private workspaceIdentifier: string,
|
||||||
private _getKeyPairs: GetKeyPairs,
|
private _getKeyPairs: GetKeyPairs,
|
||||||
|
private isApplicationUsingThirdPartyHostUseCase: IsApplicationUsingThirdPartyHost,
|
||||||
protected override internalEventBus: InternalEventBusInterface,
|
protected override internalEventBus: InternalEventBusInterface,
|
||||||
) {
|
) {
|
||||||
super(internalEventBus)
|
super(internalEventBus)
|
||||||
@@ -274,7 +276,13 @@ export class SessionManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
public isSignedIntoFirstPartyServer(): boolean {
|
public isSignedIntoFirstPartyServer(): boolean {
|
||||||
return this.isSignedIn() && !this.apiService.isThirdPartyHostUsed()
|
const isThirdPartyHostUsedOrError = this.isApplicationUsingThirdPartyHostUseCase.execute()
|
||||||
|
if (isThirdPartyHostUsedOrError.isFailed()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const isThirdPartyHostUsed = isThirdPartyHostUsedOrError.getValue()
|
||||||
|
|
||||||
|
return this.isSignedIn() && !isThirdPartyHostUsed
|
||||||
}
|
}
|
||||||
|
|
||||||
public async reauthenticateInvalidSession(
|
public async reauthenticateInvalidSession(
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ describe('features', () => {
|
|||||||
|
|
||||||
describe('extension repo items observer', () => {
|
describe('extension repo items observer', () => {
|
||||||
it('should migrate to user setting when extension repo is added', async () => {
|
it('should migrate to user setting when extension repo is added', async () => {
|
||||||
sinon.stub(application.legacyApi, 'isThirdPartyHostUsed').callsFake(() => {
|
sinon.stub(application.features.isApplicationUsingThirdPartyHostUseCase, 'execute').callsFake(() => {
|
||||||
return false
|
return Result.ok(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
@@ -74,8 +74,8 @@ describe('features', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('signing into account with ext repo should migrate it', async () => {
|
it('signing into account with ext repo should migrate it', async () => {
|
||||||
sinon.stub(application.legacyApi, 'isThirdPartyHostUsed').callsFake(() => {
|
sinon.stub(application.features.isApplicationUsingThirdPartyHostUseCase, 'execute').callsFake(() => {
|
||||||
return false
|
return Result.ok(false)
|
||||||
})
|
})
|
||||||
/** Attach an ExtensionRepo object to an account, but prevent it from being migrated.
|
/** Attach an ExtensionRepo object to an account, but prevent it from being migrated.
|
||||||
* Then sign out, sign back in, and ensure the item is migrated. */
|
* Then sign out, sign back in, and ensure the item is migrated. */
|
||||||
@@ -96,8 +96,8 @@ describe('features', () => {
|
|||||||
application = await Factory.signOutApplicationAndReturnNew(application)
|
application = await Factory.signOutApplicationAndReturnNew(application)
|
||||||
|
|
||||||
sinon.restore()
|
sinon.restore()
|
||||||
sinon.stub(application.legacyApi, 'isThirdPartyHostUsed').callsFake(() => {
|
sinon.stub(application.features.isApplicationUsingThirdPartyHostUseCase, 'execute').callsFake(() => {
|
||||||
return false
|
return Result.ok(false)
|
||||||
})
|
})
|
||||||
const promise = new Promise((resolve) => {
|
const promise = new Promise((resolve) => {
|
||||||
sinon.stub(application.features, 'migrateFeatureRepoToUserSetting').callsFake(resolve)
|
sinon.stub(application.features, 'migrateFeatureRepoToUserSetting').callsFake(resolve)
|
||||||
@@ -112,8 +112,8 @@ describe('features', () => {
|
|||||||
|
|
||||||
it('having an ext repo with no account, then signing into account, should migrate it', async () => {
|
it('having an ext repo with no account, then signing into account, should migrate it', async () => {
|
||||||
application = await Factory.signOutApplicationAndReturnNew(application)
|
application = await Factory.signOutApplicationAndReturnNew(application)
|
||||||
sinon.stub(application.legacyApi, 'isThirdPartyHostUsed').callsFake(() => {
|
sinon.stub(application.features.isApplicationUsingThirdPartyHostUseCase, 'execute').callsFake(() => {
|
||||||
return false
|
return Result.ok(false)
|
||||||
})
|
})
|
||||||
const extensionKey = UuidGenerator.GenerateUuid().split('-').join('')
|
const extensionKey = UuidGenerator.GenerateUuid().split('-').join('')
|
||||||
await application.mutator.createItem(
|
await application.mutator.createItem(
|
||||||
@@ -137,8 +137,8 @@ describe('features', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('migrated ext repo should have property indicating it was migrated', async () => {
|
it('migrated ext repo should have property indicating it was migrated', async () => {
|
||||||
sinon.stub(application.legacyApi, 'isThirdPartyHostUsed').callsFake(() => {
|
sinon.stub(application.features.isApplicationUsingThirdPartyHostUseCase, 'execute').callsFake(() => {
|
||||||
return false
|
return Result.ok(false)
|
||||||
})
|
})
|
||||||
const setting = SettingName.create(SettingName.NAMES.ExtensionKey).getValue()
|
const setting = SettingName.create(SettingName.NAMES.ExtensionKey).getValue()
|
||||||
expect(await application.settings.getDoesSensitiveSettingExist(setting)).to.equal(false)
|
expect(await application.settings.getDoesSensitiveSettingExist(setting)).to.equal(false)
|
||||||
|
|||||||
@@ -52,4 +52,6 @@ export const Web_TYPES = {
|
|||||||
LoadPurchaseFlowUrl: Symbol.for('LoadPurchaseFlowUrl'),
|
LoadPurchaseFlowUrl: Symbol.for('LoadPurchaseFlowUrl'),
|
||||||
OpenSubscriptionDashboard: Symbol.for('OpenSubscriptionDashboard'),
|
OpenSubscriptionDashboard: Symbol.for('OpenSubscriptionDashboard'),
|
||||||
PanesForLayout: Symbol.for('PanesForLayout'),
|
PanesForLayout: Symbol.for('PanesForLayout'),
|
||||||
|
GetHost: Symbol.for('GetHost'),
|
||||||
|
IsApplicationUsingThirdPartyHost: Symbol.for('IsApplicationUsingThirdPartyHost'),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
} from '@standardnotes/ui-services'
|
} from '@standardnotes/ui-services'
|
||||||
import { DependencyContainer } from '@standardnotes/utils'
|
import { DependencyContainer } from '@standardnotes/utils'
|
||||||
import { Web_TYPES } from './Types'
|
import { Web_TYPES } from './Types'
|
||||||
import { BackupServiceInterface, isDesktopDevice } from '@standardnotes/snjs'
|
import { BackupServiceInterface, GetHost, IsApplicationUsingThirdPartyHost, isDesktopDevice } from '@standardnotes/snjs'
|
||||||
import { DesktopManager } from '../Device/DesktopManager'
|
import { DesktopManager } from '../Device/DesktopManager'
|
||||||
import { MomentsService } from '@/Controllers/Moments/MomentsService'
|
import { MomentsService } from '@/Controllers/Moments/MomentsService'
|
||||||
import { PersistenceService } from '@/Controllers/Abstract/PersistenceService'
|
import { PersistenceService } from '@/Controllers/Abstract/PersistenceService'
|
||||||
@@ -195,6 +195,14 @@ export class WebDependencies extends DependencyContainer {
|
|||||||
return new PanesForLayout(this.get<IsTabletOrMobileScreen>(Web_TYPES.IsTabletOrMobileScreen))
|
return new PanesForLayout(this.get<IsTabletOrMobileScreen>(Web_TYPES.IsTabletOrMobileScreen))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.bind(Web_TYPES.GetHost, () => {
|
||||||
|
return new GetHost(application.legacyApi)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.bind(Web_TYPES.IsApplicationUsingThirdPartyHost, () => {
|
||||||
|
return new IsApplicationUsingThirdPartyHost(this.get<GetHost>(Web_TYPES.GetHost))
|
||||||
|
})
|
||||||
|
|
||||||
this.bind(Web_TYPES.IsTabletOrMobileScreen, () => {
|
this.bind(Web_TYPES.IsTabletOrMobileScreen, () => {
|
||||||
return new IsTabletOrMobileScreen(application.environment)
|
return new IsTabletOrMobileScreen(application.environment)
|
||||||
})
|
})
|
||||||
@@ -320,7 +328,11 @@ export class WebDependencies extends DependencyContainer {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.bind(Web_TYPES.GetPurchaseFlowUrl, () => {
|
this.bind(Web_TYPES.GetPurchaseFlowUrl, () => {
|
||||||
return new GetPurchaseFlowUrl(application, application.legacyApi)
|
return new GetPurchaseFlowUrl(
|
||||||
|
application,
|
||||||
|
application.legacyApi,
|
||||||
|
this.get<IsApplicationUsingThirdPartyHost>(Web_TYPES.IsApplicationUsingThirdPartyHost),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.bind(Web_TYPES.SyncStatusController, () => {
|
this.bind(Web_TYPES.SyncStatusController, () => {
|
||||||
|
|||||||
@@ -1,17 +1,30 @@
|
|||||||
import { isDesktopApplication } from '@/Utils'
|
import { isDesktopApplication } from '@/Utils'
|
||||||
import { ApplicationInterface, LegacyApiServiceInterface, Result, UseCaseInterface } from '@standardnotes/snjs'
|
import {
|
||||||
|
ApplicationInterface,
|
||||||
|
IsApplicationUsingThirdPartyHost,
|
||||||
|
LegacyApiServiceInterface,
|
||||||
|
Result,
|
||||||
|
UseCaseInterface,
|
||||||
|
} from '@standardnotes/snjs'
|
||||||
|
|
||||||
export class GetPurchaseFlowUrl implements UseCaseInterface<string> {
|
export class GetPurchaseFlowUrl implements UseCaseInterface<string> {
|
||||||
constructor(
|
constructor(
|
||||||
private application: ApplicationInterface,
|
private application: ApplicationInterface,
|
||||||
private legacyApi: LegacyApiServiceInterface,
|
private legacyApi: LegacyApiServiceInterface,
|
||||||
|
private isApplicationUsingThirdPartyHostUseCase: IsApplicationUsingThirdPartyHost,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async execute(): Promise<Result<string>> {
|
async execute(): Promise<Result<string>> {
|
||||||
const currentUrl = window.location.origin
|
const currentUrl = window.location.origin
|
||||||
const successUrl = isDesktopApplication() ? 'standardnotes://' : currentUrl
|
const successUrl = isDesktopApplication() ? 'standardnotes://' : currentUrl
|
||||||
|
|
||||||
if (this.application.sessions.isSignedOut() || this.application.isThirdPartyHostUsed()) {
|
const isThirdPartyHostUsedOrError = this.isApplicationUsingThirdPartyHostUseCase.execute()
|
||||||
|
if (isThirdPartyHostUsedOrError.isFailed()) {
|
||||||
|
return Result.fail(isThirdPartyHostUsedOrError.getError()!)
|
||||||
|
}
|
||||||
|
const isThirdPartyHostUsed = isThirdPartyHostUsedOrError.getValue()
|
||||||
|
|
||||||
|
if (this.application.sessions.isSignedOut() || isThirdPartyHostUsed) {
|
||||||
return Result.ok(`${window.purchaseUrl}/offline?&success_url=${successUrl}`)
|
return Result.ok(`${window.purchaseUrl}/offline?&success_url=${successUrl}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const AccountPreferences = ({ application }: Props) => {
|
const AccountPreferences = ({ application }: Props) => {
|
||||||
const isUsingThirdPartyServer = application.isThirdPartyHostUsed()
|
const isUsingThirdPartyServer = !application.sessions.isSignedIntoFirstPartyServer()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PreferencesPane>
|
<PreferencesPane>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const FilesSection: FunctionComponent<Props> = ({ application }) => {
|
|||||||
setFilesQuotaUsed(parseFloat(filesQuotaUsed))
|
setFilesQuotaUsed(parseFloat(filesQuotaUsed))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!application.isThirdPartyHostUsed()) {
|
if (application.sessions.isSignedIntoFirstPartyServer()) {
|
||||||
const filesQuotaTotal = await application.settings.getSubscriptionSetting(
|
const filesQuotaTotal = await application.settings.getSubscriptionSetting(
|
||||||
SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue(),
|
SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue(),
|
||||||
)
|
)
|
||||||
@@ -54,7 +54,12 @@ const FilesSection: FunctionComponent<Props> = ({ application }) => {
|
|||||||
<>
|
<>
|
||||||
<div className="mb-1 mt-1">
|
<div className="mb-1 mt-1">
|
||||||
<span className="font-semibold">{formatSizeToReadableString(filesQuotaUsed)}</span> of{' '}
|
<span className="font-semibold">{formatSizeToReadableString(filesQuotaUsed)}</span> of{' '}
|
||||||
<span>{application.isThirdPartyHostUsed() ? '∞' : formatSizeToReadableString(filesQuotaTotal)}</span> used
|
<span>
|
||||||
|
{application.sessions.isSignedIntoFirstPartyServer()
|
||||||
|
? formatSizeToReadableString(filesQuotaTotal)
|
||||||
|
: '∞'}
|
||||||
|
</span>{' '}
|
||||||
|
used
|
||||||
</div>
|
</div>
|
||||||
<progress
|
<progress
|
||||||
className="progress-bar w-full"
|
className="progress-bar w-full"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Backups: FunctionComponent<Props> = ({ application }) => {
|
const Backups: FunctionComponent<Props> = ({ application }) => {
|
||||||
const isUsingThirdPartyServer = application.isThirdPartyHostUsed()
|
const isUsingThirdPartyServer = !application.sessions.isSignedIntoFirstPartyServer()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PreferencesPane>
|
<PreferencesPane>
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ const OfflineSubscription: FunctionComponent<Props> = ({ application, onSuccess
|
|||||||
}, [application])
|
}, [application])
|
||||||
|
|
||||||
const shouldShowOfflineSubscription = () => {
|
const shouldShowOfflineSubscription = () => {
|
||||||
return !application.hasAccount() || application.isThirdPartyHostUsed() || hasUserPreviouslyStoredCode
|
return (
|
||||||
|
!application.hasAccount() || !application.sessions.isSignedIntoFirstPartyServer() || hasUserPreviouslyStoredCode
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubscriptionCodeSubmit = async (event: React.FormEvent) => {
|
const handleSubscriptionCodeSubmit = async (event: React.FormEvent) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user