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
|
||||
extends AbstractService<ApiServiceEvent, ApiServiceEventData>,
|
||||
FilesApiInterface {
|
||||
isThirdPartyHostUsed(): boolean
|
||||
setHost(host: string): Promise<void>
|
||||
getHost(): string
|
||||
|
||||
downloadOfflineFeaturesFromRepo(
|
||||
repo: SNFeatureRepo,
|
||||
): Promise<{ features: AnyFeatureDescription[]; roles: string[] } | ClientDisplayableError>
|
||||
downloadOfflineFeaturesFromRepo(dto: {
|
||||
repo: SNFeatureRepo
|
||||
trustedFeatureHosts: string[]
|
||||
}): Promise<{ features: AnyFeatureDescription[]; roles: string[] } | ClientDisplayableError>
|
||||
|
||||
downloadFeatureUrl(url: string): Promise<HttpResponse>
|
||||
|
||||
|
||||
@@ -66,7 +66,6 @@ export interface ApplicationInterface {
|
||||
|
||||
hasAccount(): boolean
|
||||
setCustomHost(host: string): Promise<void>
|
||||
isThirdPartyHostUsed(): boolean
|
||||
isUsingHomeServer(): Promise<boolean>
|
||||
|
||||
importData(data: BackupFile, awaitSync?: boolean): Promise<ImportDataReturnType>
|
||||
|
||||
@@ -7,14 +7,17 @@ import { SubscriptionApiServiceInterface } from '@standardnotes/api'
|
||||
import { Invitation } from '@standardnotes/models'
|
||||
import { InternalEventBusInterface } from '..'
|
||||
import { SubscriptionManager } from './SubscriptionManager'
|
||||
import { IsApplicationUsingThirdPartyHost } from '../UseCase/IsApplicationUsingThirdPartyHost'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
|
||||
describe('SubscriptionManager', () => {
|
||||
let subscriptionApiService: SubscriptionApiServiceInterface
|
||||
let internalEventBus: InternalEventBusInterface
|
||||
let sessions: SessionsClientInterface
|
||||
let storage: StorageServiceInterface
|
||||
let isApplicationUsingThirdPartyHostUseCase: IsApplicationUsingThirdPartyHost
|
||||
|
||||
const createManager = () => new SubscriptionManager(subscriptionApiService, sessions, storage, internalEventBus)
|
||||
const createManager = () => new SubscriptionManager(subscriptionApiService, sessions, storage, isApplicationUsingThirdPartyHostUseCase, internalEventBus)
|
||||
|
||||
beforeEach(() => {
|
||||
subscriptionApiService = {} as jest.Mocked<SubscriptionApiServiceInterface>
|
||||
@@ -31,6 +34,9 @@ describe('SubscriptionManager', () => {
|
||||
internalEventBus = {} as jest.Mocked<InternalEventBusInterface>
|
||||
internalEventBus.addEventHandler = jest.fn()
|
||||
internalEventBus.publish = jest.fn()
|
||||
|
||||
isApplicationUsingThirdPartyHostUseCase = {} as jest.Mocked<IsApplicationUsingThirdPartyHost>
|
||||
isApplicationUsingThirdPartyHostUseCase.execute = jest.fn().mockReturnValue(Result.ok(false))
|
||||
})
|
||||
|
||||
describe('event handling', () => {
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from '@standardnotes/responses'
|
||||
import { SubscriptionManagerEvent } from './SubscriptionManagerEvent'
|
||||
import { ApplicationStageChangedEventPayload } from '../Event/ApplicationStageChangedEventPayload'
|
||||
import { IsApplicationUsingThirdPartyHost } from '../UseCase/IsApplicationUsingThirdPartyHost'
|
||||
|
||||
export class SubscriptionManager
|
||||
extends AbstractService<SubscriptionManagerEvent>
|
||||
@@ -34,6 +35,7 @@ export class SubscriptionManager
|
||||
private subscriptionApiService: SubscriptionApiServiceInterface,
|
||||
private sessions: SessionsClientInterface,
|
||||
private storage: StorageServiceInterface,
|
||||
private isApplicationUsingThirdPartyHostUseCase: IsApplicationUsingThirdPartyHost,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
@@ -43,7 +45,15 @@ export class SubscriptionManager
|
||||
switch (event.type) {
|
||||
case ApplicationEvent.Launched: {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -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/GenerateUuid'
|
||||
export * from './UseCase/GetHost'
|
||||
export * from './UseCase/IsApplicationUsingThirdPartyHost'
|
||||
export * from './UseCase/SetHost'
|
||||
export * from './User/AccountEvent'
|
||||
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> {
|
||||
const homeServerService = this.dependencies.get<HomeServerServiceInterface>(TYPES.HomeServerService)
|
||||
|
||||
|
||||
@@ -133,6 +133,7 @@ import {
|
||||
GenerateUuid,
|
||||
GetVaultItems,
|
||||
ValidateVaultPassword,
|
||||
IsApplicationUsingThirdPartyHost,
|
||||
} from '@standardnotes/services'
|
||||
import { ItemManager } from '../../Services/Items/ItemManager'
|
||||
import { PayloadManager } from '../../Services/Payloads/PayloadManager'
|
||||
@@ -243,6 +244,10 @@ export class Dependencies {
|
||||
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, () => {
|
||||
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<PureCryptoInterface>(TYPES.Crypto),
|
||||
this.get<Logger>(TYPES.Logger),
|
||||
this.get<IsApplicationUsingThirdPartyHost>(TYPES.IsApplicationUsingThirdPartyHost),
|
||||
this.get<InternalEventBus>(TYPES.InternalEventBus),
|
||||
)
|
||||
})
|
||||
@@ -1267,6 +1273,7 @@ export class Dependencies {
|
||||
this.get<SubscriptionApiService>(TYPES.SubscriptionApiService),
|
||||
this.get<SessionManager>(TYPES.SessionManager),
|
||||
this.get<DiskStorageService>(TYPES.DiskStorageService),
|
||||
this.get<IsApplicationUsingThirdPartyHost>(TYPES.IsApplicationUsingThirdPartyHost),
|
||||
this.get<InternalEventBus>(TYPES.InternalEventBus),
|
||||
)
|
||||
})
|
||||
@@ -1286,6 +1293,7 @@ export class Dependencies {
|
||||
this.get<LegacySessionStorageMapper>(TYPES.LegacySessionStorageMapper),
|
||||
this.options.identifier,
|
||||
this.get<GetKeyPairs>(TYPES.GetKeyPairs),
|
||||
this.get<IsApplicationUsingThirdPartyHost>(TYPES.IsApplicationUsingThirdPartyHost),
|
||||
this.get<InternalEventBus>(TYPES.InternalEventBus),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -158,6 +158,7 @@ export const TYPES = {
|
||||
ChangeVaultStorageMode: Symbol.for('ChangeVaultStorageMode'),
|
||||
ChangeAndSaveItem: Symbol.for('ChangeAndSaveItem'),
|
||||
GetHost: Symbol.for('GetHost'),
|
||||
IsApplicationUsingThirdPartyHost: Symbol.for('IsApplicationUsingThirdPartyHost'),
|
||||
SetHost: Symbol.for('SetHost'),
|
||||
GenerateUuid: Symbol.for('GenerateUuid'),
|
||||
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 { SNRootKeyParams } from '@standardnotes/encryption'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { isUrlFirstParty, TRUSTED_FEATURE_HOSTS } from '@Lib/Hosts'
|
||||
import { Paths } from './Paths'
|
||||
import { DiskStorageService } from '../Storage/DiskStorageService'
|
||||
import { UuidString } from '../../Types/UuidString'
|
||||
@@ -157,11 +156,6 @@ export class LegacyApiService
|
||||
return this.host
|
||||
}
|
||||
|
||||
public isThirdPartyHostUsed(): boolean {
|
||||
const applicationHost = this.getHost() || ''
|
||||
return !isUrlFirstParty(applicationHost)
|
||||
}
|
||||
|
||||
public getFilesHost(): string {
|
||||
if (!this.filesHost) {
|
||||
throw Error('Attempting to access undefined filesHost')
|
||||
@@ -620,19 +614,20 @@ export class LegacyApiService
|
||||
return response.data.token
|
||||
}
|
||||
|
||||
public async downloadOfflineFeaturesFromRepo(
|
||||
repo: SNFeatureRepo,
|
||||
): Promise<{ features: AnyFeatureDescription[]; roles: string[] } | ClientDisplayableError> {
|
||||
public async downloadOfflineFeaturesFromRepo(dto: {
|
||||
repo: SNFeatureRepo
|
||||
trustedFeatureHosts: string[]
|
||||
}): Promise<{ features: AnyFeatureDescription[]; roles: string[] } | ClientDisplayableError> {
|
||||
try {
|
||||
const featuresUrl = repo.offlineFeaturesUrl
|
||||
const extensionKey = repo.offlineKey
|
||||
const featuresUrl = dto.repo.offlineFeaturesUrl
|
||||
const extensionKey = dto.repo.offlineKey
|
||||
if (!featuresUrl || !extensionKey) {
|
||||
throw Error('Cannot download offline repo without url and offlineKEy')
|
||||
}
|
||||
|
||||
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.`)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ItemInterface, SNFeatureRepo } from '@standardnotes/models'
|
||||
import { SyncService } from '../Sync/SyncService'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { FeaturesService } from '@Lib/Services/Features'
|
||||
import { RoleName, ContentType, Uuid } from '@standardnotes/domain-core'
|
||||
import { RoleName, ContentType, Uuid, Result } from '@standardnotes/domain-core'
|
||||
import { NativeFeatureIdentifier, GetFeatures } from '@standardnotes/features'
|
||||
import { WebSocketsService } from '../Api/WebsocketsService'
|
||||
import { SettingsService } from '../Settings'
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
SyncServiceInterface,
|
||||
UserServiceInterface,
|
||||
UserService,
|
||||
IsApplicationUsingThirdPartyHost,
|
||||
} from '@standardnotes/services'
|
||||
import { LegacyApiService, SessionManager } from '../Api'
|
||||
import { ItemManager } from '../Items'
|
||||
@@ -47,6 +48,7 @@ describe('FeaturesService', () => {
|
||||
let internalEventBus: InternalEventBusInterface
|
||||
let featureService: FeaturesService
|
||||
let logger: LoggerInterface
|
||||
let isApplicationUsingThirdPartyHostUseCase: IsApplicationUsingThirdPartyHost
|
||||
|
||||
beforeEach(() => {
|
||||
logger = {} as jest.Mocked<LoggerInterface>
|
||||
@@ -62,7 +64,6 @@ describe('FeaturesService', () => {
|
||||
|
||||
apiService = {} as jest.Mocked<LegacyApiService>
|
||||
apiService.addEventObserver = jest.fn()
|
||||
apiService.isThirdPartyHostUsed = jest.fn().mockReturnValue(false)
|
||||
|
||||
itemManager = {} as jest.Mocked<ItemManager>
|
||||
itemManager.getItems = jest.fn().mockReturnValue(items)
|
||||
@@ -107,6 +108,9 @@ describe('FeaturesService', () => {
|
||||
internalEventBus.publish = jest.fn()
|
||||
internalEventBus.addEventHandler = jest.fn()
|
||||
|
||||
isApplicationUsingThirdPartyHostUseCase = {} as jest.Mocked<IsApplicationUsingThirdPartyHost>
|
||||
isApplicationUsingThirdPartyHostUseCase.execute = jest.fn().mockReturnValue(Result.ok(false))
|
||||
|
||||
featureService = new FeaturesService(
|
||||
storageService,
|
||||
itemManager,
|
||||
@@ -121,6 +125,7 @@ describe('FeaturesService', () => {
|
||||
sessionManager,
|
||||
crypto,
|
||||
logger,
|
||||
isApplicationUsingThirdPartyHostUseCase,
|
||||
internalEventBus,
|
||||
)
|
||||
})
|
||||
@@ -202,6 +207,7 @@ describe('FeaturesService', () => {
|
||||
sessionManager,
|
||||
crypto,
|
||||
logger,
|
||||
isApplicationUsingThirdPartyHostUseCase,
|
||||
internalEventBus,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,11 +2,9 @@ import { MigrateFeatureRepoToUserSettingUseCase } from './UseCase/MigrateFeature
|
||||
import { arraysEqual, removeFromArray, lastElement, LoggerInterface } from '@standardnotes/utils'
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
import { RoleName, ContentType, Uuid } from '@standardnotes/domain-core'
|
||||
import { PROD_OFFLINE_FEATURES_URL } from '../../Hosts'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { WebSocketsService } from '../Api/WebsocketsService'
|
||||
import { WebSocketsServiceEvent } from '../Api/WebSocketsServiceEvent'
|
||||
import { TRUSTED_CUSTOM_EXTENSIONS_HOSTS, TRUSTED_FEATURE_HOSTS } from '@Lib/Hosts'
|
||||
import { UserRolesChangedEvent } from '@standardnotes/domain-events'
|
||||
import { ExperimentalFeatures, FindNativeFeature, NativeFeatureIdentifier } from '@standardnotes/features'
|
||||
import {
|
||||
@@ -48,6 +46,7 @@ import {
|
||||
SubscriptionManagerEvent,
|
||||
ApplicationEvent,
|
||||
ApplicationStageChangedEventPayload,
|
||||
IsApplicationUsingThirdPartyHost,
|
||||
} from '@standardnotes/services'
|
||||
|
||||
import { DownloadRemoteThirdPartyFeatureUseCase } from './UseCase/DownloadRemoteThirdPartyFeature'
|
||||
@@ -67,6 +66,18 @@ export class FeaturesService
|
||||
|
||||
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(
|
||||
private storage: StorageServiceInterface,
|
||||
private items: ItemManagerInterface,
|
||||
@@ -81,6 +92,7 @@ export class FeaturesService
|
||||
private sessions: SessionsClientInterface,
|
||||
private crypto: PureCryptoInterface,
|
||||
private logger: LoggerInterface,
|
||||
private isApplicationUsingThirdPartyHostUseCase: IsApplicationUsingThirdPartyHost,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
@@ -128,7 +140,12 @@ export class FeaturesService
|
||||
if (eventName === AccountEvent.SignedInOrRegistered) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -285,7 +302,10 @@ export class FeaturesService
|
||||
}
|
||||
|
||||
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) {
|
||||
return result
|
||||
@@ -301,7 +321,7 @@ export class FeaturesService
|
||||
|
||||
public async migrateFeatureRepoToOfflineEntitlements(featureRepos: SNFeatureRepo[] = []): Promise<void> {
|
||||
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) {
|
||||
await this.downloadOfflineRoles(updatedRepos[0])
|
||||
@@ -322,7 +342,7 @@ export class FeaturesService
|
||||
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'
|
||||
}
|
||||
|
||||
@@ -427,7 +447,7 @@ export class FeaturesService
|
||||
}
|
||||
|
||||
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 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 { MutatorClientInterface } from '@standardnotes/services'
|
||||
|
||||
export class MigrateFeatureRepoToOfflineEntitlementsUseCase {
|
||||
private readonly LEGACY_PROD_EXT_ORIGIN = 'https://extensions.standardnotes.org'
|
||||
|
||||
constructor(private mutator: MutatorClientInterface) {}
|
||||
|
||||
async execute(featureRepos: SNFeatureRepo[] = []): Promise<SNFeatureRepo[]> {
|
||||
async execute(dto: { featureRepos: SNFeatureRepo[]; prodOfflineFeaturesUrl: string }): Promise<SNFeatureRepo[]> {
|
||||
const updatedRepos: SNFeatureRepo[] = []
|
||||
for (const item of featureRepos) {
|
||||
for (const item of dto.featureRepos) {
|
||||
if (item.migratedToOfflineEntitlements) {
|
||||
continue
|
||||
}
|
||||
@@ -19,7 +20,7 @@ export class MigrateFeatureRepoToOfflineEntitlementsUseCase {
|
||||
const repoUrl = item.onlineUrl
|
||||
const { origin } = new URL(repoUrl)
|
||||
|
||||
if (!origin.includes(LEGACY_PROD_EXT_ORIGIN)) {
|
||||
if (!origin.includes(this.LEGACY_PROD_EXT_ORIGIN)) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -28,7 +29,7 @@ export class MigrateFeatureRepoToOfflineEntitlementsUseCase {
|
||||
const userKey = userKeyMatch[0]
|
||||
|
||||
const updatedRepo = await this.mutator.changeFeatureRepo(item, (m) => {
|
||||
m.offlineFeaturesUrl = PROD_OFFLINE_FEATURES_URL
|
||||
m.offlineFeaturesUrl = dto.prodOfflineFeaturesUrl
|
||||
m.offlineKey = userKey
|
||||
m.migratedToOfflineEntitlements = true
|
||||
})
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
ApplicationStageChangedEventPayload,
|
||||
ApplicationStage,
|
||||
GetKeyPairs,
|
||||
IsApplicationUsingThirdPartyHost,
|
||||
} from '@standardnotes/services'
|
||||
import { Base64String, PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import {
|
||||
@@ -105,6 +106,7 @@ export class SessionManager
|
||||
private legacySessionStorageMapper: MapperInterface<LegacySession, Record<string, unknown>>,
|
||||
private workspaceIdentifier: string,
|
||||
private _getKeyPairs: GetKeyPairs,
|
||||
private isApplicationUsingThirdPartyHostUseCase: IsApplicationUsingThirdPartyHost,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
@@ -274,7 +276,13 @@ export class SessionManager
|
||||
}
|
||||
|
||||
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(
|
||||
|
||||
@@ -47,8 +47,8 @@ describe('features', () => {
|
||||
|
||||
describe('extension repo items observer', () => {
|
||||
it('should migrate to user setting when extension repo is added', async () => {
|
||||
sinon.stub(application.legacyApi, 'isThirdPartyHostUsed').callsFake(() => {
|
||||
return false
|
||||
sinon.stub(application.features.isApplicationUsingThirdPartyHostUseCase, 'execute').callsFake(() => {
|
||||
return Result.ok(false)
|
||||
})
|
||||
|
||||
expect(
|
||||
@@ -74,8 +74,8 @@ describe('features', () => {
|
||||
})
|
||||
|
||||
it('signing into account with ext repo should migrate it', async () => {
|
||||
sinon.stub(application.legacyApi, 'isThirdPartyHostUsed').callsFake(() => {
|
||||
return false
|
||||
sinon.stub(application.features.isApplicationUsingThirdPartyHostUseCase, 'execute').callsFake(() => {
|
||||
return Result.ok(false)
|
||||
})
|
||||
/** 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. */
|
||||
@@ -96,8 +96,8 @@ describe('features', () => {
|
||||
application = await Factory.signOutApplicationAndReturnNew(application)
|
||||
|
||||
sinon.restore()
|
||||
sinon.stub(application.legacyApi, 'isThirdPartyHostUsed').callsFake(() => {
|
||||
return false
|
||||
sinon.stub(application.features.isApplicationUsingThirdPartyHostUseCase, 'execute').callsFake(() => {
|
||||
return Result.ok(false)
|
||||
})
|
||||
const promise = new Promise((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 () => {
|
||||
application = await Factory.signOutApplicationAndReturnNew(application)
|
||||
sinon.stub(application.legacyApi, 'isThirdPartyHostUsed').callsFake(() => {
|
||||
return false
|
||||
sinon.stub(application.features.isApplicationUsingThirdPartyHostUseCase, 'execute').callsFake(() => {
|
||||
return Result.ok(false)
|
||||
})
|
||||
const extensionKey = UuidGenerator.GenerateUuid().split('-').join('')
|
||||
await application.mutator.createItem(
|
||||
@@ -137,8 +137,8 @@ describe('features', () => {
|
||||
})
|
||||
|
||||
it('migrated ext repo should have property indicating it was migrated', async () => {
|
||||
sinon.stub(application.legacyApi, 'isThirdPartyHostUsed').callsFake(() => {
|
||||
return false
|
||||
sinon.stub(application.features.isApplicationUsingThirdPartyHostUseCase, 'execute').callsFake(() => {
|
||||
return Result.ok(false)
|
||||
})
|
||||
const setting = SettingName.create(SettingName.NAMES.ExtensionKey).getValue()
|
||||
expect(await application.settings.getDoesSensitiveSettingExist(setting)).to.equal(false)
|
||||
|
||||
@@ -52,4 +52,6 @@ export const Web_TYPES = {
|
||||
LoadPurchaseFlowUrl: Symbol.for('LoadPurchaseFlowUrl'),
|
||||
OpenSubscriptionDashboard: Symbol.for('OpenSubscriptionDashboard'),
|
||||
PanesForLayout: Symbol.for('PanesForLayout'),
|
||||
GetHost: Symbol.for('GetHost'),
|
||||
IsApplicationUsingThirdPartyHost: Symbol.for('IsApplicationUsingThirdPartyHost'),
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from '@standardnotes/ui-services'
|
||||
import { DependencyContainer } from '@standardnotes/utils'
|
||||
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 { MomentsService } from '@/Controllers/Moments/MomentsService'
|
||||
import { PersistenceService } from '@/Controllers/Abstract/PersistenceService'
|
||||
@@ -195,6 +195,14 @@ export class WebDependencies extends DependencyContainer {
|
||||
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, () => {
|
||||
return new IsTabletOrMobileScreen(application.environment)
|
||||
})
|
||||
@@ -320,7 +328,11 @@ export class WebDependencies extends DependencyContainer {
|
||||
})
|
||||
|
||||
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, () => {
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
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> {
|
||||
constructor(
|
||||
private application: ApplicationInterface,
|
||||
private legacyApi: LegacyApiServiceInterface,
|
||||
private isApplicationUsingThirdPartyHostUseCase: IsApplicationUsingThirdPartyHost,
|
||||
) {}
|
||||
|
||||
async execute(): Promise<Result<string>> {
|
||||
const currentUrl = window.location.origin
|
||||
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}`)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ type Props = {
|
||||
}
|
||||
|
||||
const AccountPreferences = ({ application }: Props) => {
|
||||
const isUsingThirdPartyServer = application.isThirdPartyHostUsed()
|
||||
const isUsingThirdPartyServer = !application.sessions.isSignedIntoFirstPartyServer()
|
||||
|
||||
return (
|
||||
<PreferencesPane>
|
||||
|
||||
@@ -25,7 +25,7 @@ const FilesSection: FunctionComponent<Props> = ({ application }) => {
|
||||
setFilesQuotaUsed(parseFloat(filesQuotaUsed))
|
||||
}
|
||||
|
||||
if (!application.isThirdPartyHostUsed()) {
|
||||
if (application.sessions.isSignedIntoFirstPartyServer()) {
|
||||
const filesQuotaTotal = await application.settings.getSubscriptionSetting(
|
||||
SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue(),
|
||||
)
|
||||
@@ -54,7 +54,12 @@ const FilesSection: FunctionComponent<Props> = ({ application }) => {
|
||||
<>
|
||||
<div className="mb-1 mt-1">
|
||||
<span className="font-semibold">{formatSizeToReadableString(filesQuotaUsed)}</span> of{' '}
|
||||
<span>{application.isThirdPartyHostUsed() ? '∞' : formatSizeToReadableString(filesQuotaTotal)}</span> used
|
||||
<span>
|
||||
{application.sessions.isSignedIntoFirstPartyServer()
|
||||
? formatSizeToReadableString(filesQuotaTotal)
|
||||
: '∞'}
|
||||
</span>{' '}
|
||||
used
|
||||
</div>
|
||||
<progress
|
||||
className="progress-bar w-full"
|
||||
|
||||
@@ -13,7 +13,7 @@ type Props = {
|
||||
}
|
||||
|
||||
const Backups: FunctionComponent<Props> = ({ application }) => {
|
||||
const isUsingThirdPartyServer = application.isThirdPartyHostUsed()
|
||||
const isUsingThirdPartyServer = !application.sessions.isSignedIntoFirstPartyServer()
|
||||
|
||||
return (
|
||||
<PreferencesPane>
|
||||
|
||||
@@ -26,7 +26,9 @@ const OfflineSubscription: FunctionComponent<Props> = ({ application, onSuccess
|
||||
}, [application])
|
||||
|
||||
const shouldShowOfflineSubscription = () => {
|
||||
return !application.hasAccount() || application.isThirdPartyHostUsed() || hasUserPreviouslyStoredCode
|
||||
return (
|
||||
!application.hasAccount() || !application.sessions.isSignedIntoFirstPartyServer() || hasUserPreviouslyStoredCode
|
||||
)
|
||||
}
|
||||
|
||||
const handleSubscriptionCodeSubmit = async (event: React.FormEvent) => {
|
||||
|
||||
Reference in New Issue
Block a user