chore: remove calling payments server for subscriptions if using third party api hosts (#2398)

This commit is contained in:
Karol Sójko
2023-08-09 13:16:19 +02:00
committed by GitHub
parent e05d8c9e76
commit 90dcb33a44
24 changed files with 233 additions and 89 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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', () => {

View File

@@ -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
}

View File

@@ -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)
})
})

View File

@@ -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
}
}
}

View File

@@ -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'

View File

@@ -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)

View File

@@ -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),
)
})

View File

@@ -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'),

View File

@@ -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']

View File

@@ -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.`)
}

View File

@@ -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,
)
}

View File

@@ -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)

View File

@@ -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
})

View File

@@ -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(

View File

@@ -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)

View File

@@ -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'),
}

View File

@@ -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, () => {

View File

@@ -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}`)
}

View File

@@ -17,7 +17,7 @@ type Props = {
}
const AccountPreferences = ({ application }: Props) => {
const isUsingThirdPartyServer = application.isThirdPartyHostUsed()
const isUsingThirdPartyServer = !application.sessions.isSignedIntoFirstPartyServer()
return (
<PreferencesPane>

View File

@@ -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"

View File

@@ -13,7 +13,7 @@ type Props = {
}
const Backups: FunctionComponent<Props> = ({ application }) => {
const isUsingThirdPartyServer = application.isThirdPartyHostUsed()
const isUsingThirdPartyServer = !application.sessions.isSignedIntoFirstPartyServer()
return (
<PreferencesPane>

View File

@@ -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) => {