diff --git a/packages/api/src/Domain/Response/Subscription/SubscriptionInviteAcceptResponseBody.ts b/packages/api/src/Domain/Response/Subscription/SubscriptionInviteAcceptResponseBody.ts index e5a976440..30ea5bf7d 100644 --- a/packages/api/src/Domain/Response/Subscription/SubscriptionInviteAcceptResponseBody.ts +++ b/packages/api/src/Domain/Response/Subscription/SubscriptionInviteAcceptResponseBody.ts @@ -1,3 +1 @@ -import { Either } from '@standardnotes/common' - -export type SubscriptionInviteAcceptResponseBody = Either<{ success: true }, { success: false; message: string }> +export type SubscriptionInviteAcceptResponseBody = { success: true } | { success: false; message: string } diff --git a/packages/snjs/lib/Services/Session/SessionManager.ts b/packages/snjs/lib/Services/Session/SessionManager.ts index 98d238250..b62cb3028 100644 --- a/packages/snjs/lib/Services/Session/SessionManager.ts +++ b/packages/snjs/lib/Services/Session/SessionManager.ts @@ -139,7 +139,7 @@ export class SNSessionManager extends AbstractService implements S return isNullOrUndefined(this.apiService.getSession()) } - public getUser() { + public getUser(): Responses.User | undefined { return this.user } diff --git a/packages/snjs/lib/Services/Session/SessionsClientInterface.ts b/packages/snjs/lib/Services/Session/SessionsClientInterface.ts index 78ab38a81..c80f4f9dc 100644 --- a/packages/snjs/lib/Services/Session/SessionsClientInterface.ts +++ b/packages/snjs/lib/Services/Session/SessionsClientInterface.ts @@ -1,8 +1,8 @@ -import { ClientDisplayableError } from '@standardnotes/responses' +import { ClientDisplayableError, User } from '@standardnotes/responses' import { Base64String } from '@standardnotes/sncrypto-common' export interface SessionsClientInterface { createDemoShareToken(): Promise - populateSessionFromDemoShareToken(token: Base64String): Promise + getUser(): User | undefined } diff --git a/packages/ui-services/src/Route/RouteService.ts b/packages/ui-services/src/Route/RouteService.ts index c91965584..3c4492044 100644 --- a/packages/ui-services/src/Route/RouteService.ts +++ b/packages/ui-services/src/Route/RouteService.ts @@ -4,11 +4,16 @@ import { ApplicationInterface, InternalEventBusInterface, } from '@standardnotes/services' + import { RouteParser } from './RouteParser' import { RouteParserInterface } from './RouteParserInterface' import { RouteServiceEvent } from './RouteServiceEvent' +import { RouteServiceInterface } from './RouteServiceInterface' -export class RouteService extends AbstractService { +export class RouteService + extends AbstractService + implements RouteServiceInterface +{ private unsubApp!: () => void constructor( diff --git a/packages/ui-services/src/Route/RouteServiceInterface.ts b/packages/ui-services/src/Route/RouteServiceInterface.ts new file mode 100644 index 000000000..7a66ff1db --- /dev/null +++ b/packages/ui-services/src/Route/RouteServiceInterface.ts @@ -0,0 +1,7 @@ +import { RouteParserInterface } from './RouteParserInterface' + +export interface RouteServiceInterface { + deinit(): void + getRoute(): RouteParserInterface + removeSettingsFromURLQueryParameters(): void +} diff --git a/packages/ui-services/src/index.ts b/packages/ui-services/src/index.ts index 974ef60f3..5202f2d9f 100644 --- a/packages/ui-services/src/index.ts +++ b/packages/ui-services/src/index.ts @@ -11,6 +11,7 @@ export * from './Route/Params/SubscriptionInviteParams' export * from './Route/RouteParser' export * from './Route/RouteType' export * from './Route/RouteService' +export * from './Route/RouteServiceInterface' export * from './Route/RouteServiceEvent' export * from './Security/AutolockService' export * from './Storage/LocalStorage' diff --git a/packages/web/src/javascripts/Application/Application.ts b/packages/web/src/javascripts/Application/Application.ts index 2d8f262b4..feb5e72d6 100644 --- a/packages/web/src/javascripts/Application/Application.ts +++ b/packages/web/src/javascripts/Application/Application.ts @@ -31,6 +31,7 @@ import { AutolockService, IOService, RouteService, + RouteServiceInterface, ThemeManager, WebAlertService, } from '@standardnotes/ui-services' @@ -50,7 +51,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter private onVisibilityChange: () => void private mobileWebReceiver?: MobileWebReceiver private androidBackHandler?: AndroidBackHandler - public readonly routeService: RouteService + public readonly routeService: RouteServiceInterface constructor( deviceInterface: WebOrDesktopDevice, diff --git a/packages/web/src/javascripts/Controllers/ViewControllerManager.ts b/packages/web/src/javascripts/Controllers/ViewControllerManager.ts index d82433cfa..214bc8816 100644 --- a/packages/web/src/javascripts/Controllers/ViewControllerManager.ts +++ b/packages/web/src/javascripts/Controllers/ViewControllerManager.ts @@ -1,10 +1,9 @@ import { PaneController } from './PaneController' -import { RouteType, storage, StorageKey } from '@standardnotes/ui-services' +import { storage, StorageKey } from '@standardnotes/ui-services' import { WebApplication } from '@/Application/Application' import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController' import { destroyAllObjectProperties } from '@/Utils' import { - ApplicationEvent, DeinitSource, WebOrDesktopDeviceInterface, InternalEventBus, @@ -31,10 +30,11 @@ import { NavigationController, NavigationControllerPersistableValue } from './Na import { FilePreviewModalController } from './FilePreviewModalController' import { SelectedItemsController, SelectionControllerPersistableValue } from './SelectedItemsController' import { HistoryModalController } from './NoteHistory/HistoryModalController' -import { AccountMenuPane } from '@/Components/AccountMenu/AccountMenuPane' import { LinkingController } from './LinkingController' import { MasterPersistedValue, PersistenceKey, PersistenceService } from './Abstract/PersistenceService' import { CrossControllerEvent } from './CrossControllerEvent' +import { EventObserverInterface } from '@/Event/EventObserverInterface' +import { ApplicationEventObserver } from '@/Event/ApplicationEventObserver' export class ViewControllerManager implements InternalEventHandlerInterface { readonly enableUnfinishedFeatures: boolean = window?.enabledUnfinishedFeatures @@ -70,6 +70,7 @@ export class ViewControllerManager implements InternalEventHandlerInterface { private itemCounter: ItemCounterInterface private subscriptionManager: SubscriptionClientInterface private persistenceService: PersistenceService + private applicationEventObserver: EventObserverInterface constructor(public application: WebApplication, private device: WebOrDesktopDeviceInterface) { this.eventBus = new InternalEventBus() @@ -145,6 +146,16 @@ export class ViewControllerManager implements InternalEventHandlerInterface { this.historyModalController = new HistoryModalController(this.application, this.eventBus) + this.applicationEventObserver = new ApplicationEventObserver( + application.routeService, + this.purchaseFlowController, + this.accountMenuController, + this.preferencesController, + this.syncStatusController, + application.sync, + application.sessions, + ) + this.addAppEventObserver() if (this.device.appVersion.includes('-beta')) { @@ -240,42 +251,9 @@ export class ViewControllerManager implements InternalEventHandlerInterface { } addAppEventObserver() { - this.unsubAppEventObserver = this.application.addEventObserver(async (eventName) => { - switch (eventName) { - case ApplicationEvent.Launched: - { - const route = this.application.routeService.getRoute() - if (route.type === RouteType.Purchase) { - this.purchaseFlowController.openPurchaseFlow() - } - if (route.type === RouteType.Settings) { - const user = this.application.getUser() - if (user === undefined) { - this.accountMenuController.setShow(true) - this.accountMenuController.setCurrentPane(AccountMenuPane.SignIn) - - break - } - - this.preferencesController.openPreferences() - this.preferencesController.setCurrentPane(route.settingsParams.panel) - } - } - break - case ApplicationEvent.SignedIn: - { - const route = this.application.routeService.getRoute() - if (route.type === RouteType.Settings) { - this.preferencesController.openPreferences() - this.preferencesController.setCurrentPane(route.settingsParams.panel) - } - } - break - case ApplicationEvent.SyncStatusChanged: - this.syncStatusController.update(this.application.sync.getSyncStatus()) - break - } - }) + this.unsubAppEventObserver = this.application.addEventObserver( + this.applicationEventObserver.handle.bind(this.applicationEventObserver), + ) } persistValues = (): void => { diff --git a/packages/web/src/javascripts/Event/ApplicationEventObserver.spec.ts b/packages/web/src/javascripts/Event/ApplicationEventObserver.spec.ts new file mode 100644 index 000000000..cd0c3e3ca --- /dev/null +++ b/packages/web/src/javascripts/Event/ApplicationEventObserver.spec.ts @@ -0,0 +1,130 @@ +/** + * @jest-environment jsdom + */ + +import { RouteServiceInterface, RouteType } from '@standardnotes/ui-services' +import { ApplicationEvent, SessionsClientInterface, SyncClientInterface, SyncOpStatus, User } from '@standardnotes/snjs' + +import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController' +import { PreferencesController } from '@/Controllers/PreferencesController' +import { PurchaseFlowController } from '@/Controllers/PurchaseFlow/PurchaseFlowController' +import { SyncStatusController } from '@/Controllers/SyncStatusController' + +import { ApplicationEventObserver } from './ApplicationEventObserver' +import { RouteParserInterface } from '@standardnotes/ui-services/dist/Route/RouteParserInterface' +import { AccountMenuPane } from '@/Components/AccountMenu/AccountMenuPane' + +describe('ApplicationEventObserver', () => { + let routeService: RouteServiceInterface + let purchaseFlowController: PurchaseFlowController + let accountMenuController: AccountMenuController + let preferencesController: PreferencesController + let syncStatusController: SyncStatusController + let syncClient: SyncClientInterface + let sessionManager: SessionsClientInterface + + const createObserver = () => + new ApplicationEventObserver( + routeService, + purchaseFlowController, + accountMenuController, + preferencesController, + syncStatusController, + syncClient, + sessionManager, + ) + + beforeEach(() => { + routeService = {} as jest.Mocked + routeService.getRoute = jest.fn().mockReturnValue({ + type: RouteType.None, + } as jest.Mocked) + + purchaseFlowController = {} as jest.Mocked + purchaseFlowController.openPurchaseFlow = jest.fn() + + accountMenuController = {} as jest.Mocked + accountMenuController.setShow = jest.fn() + accountMenuController.setCurrentPane = jest.fn() + + preferencesController = {} as jest.Mocked + preferencesController.openPreferences = jest.fn() + preferencesController.setCurrentPane = jest.fn() + + syncStatusController = {} as jest.Mocked + syncStatusController.update = jest.fn() + + syncClient = {} as jest.Mocked + syncClient.getSyncStatus = jest.fn().mockReturnValue({} as jest.Mocked) + + sessionManager = {} as jest.Mocked + sessionManager.getUser = jest.fn().mockReturnValue({} as jest.Mocked) + }) + + describe('Upon Application Launched', () => { + it('should open up the purchase flow', async () => { + routeService.getRoute = jest.fn().mockReturnValue({ + type: RouteType.Purchase, + } as jest.Mocked) + + await createObserver().handle(ApplicationEvent.Launched) + + expect(purchaseFlowController.openPurchaseFlow).toHaveBeenCalled() + }) + + it('should open up settings if user is logged in', async () => { + routeService.getRoute = jest.fn().mockReturnValue({ + type: RouteType.Settings, + settingsParams: { + panel: 'general', + }, + } as jest.Mocked) + + await createObserver().handle(ApplicationEvent.Launched) + + expect(preferencesController.openPreferences).toHaveBeenCalled() + expect(preferencesController.setCurrentPane).toHaveBeenCalledWith('general') + }) + + it('should open up sign in if user is not logged in and tries to access settings', async () => { + sessionManager.getUser = jest.fn().mockReturnValue(undefined) + routeService.getRoute = jest.fn().mockReturnValue({ + type: RouteType.Settings, + settingsParams: { + panel: 'general', + }, + } as jest.Mocked) + + await createObserver().handle(ApplicationEvent.Launched) + + expect(accountMenuController.setShow).toHaveBeenCalledWith(true) + expect(accountMenuController.setCurrentPane).toHaveBeenCalledWith(AccountMenuPane.SignIn) + expect(preferencesController.openPreferences).not.toHaveBeenCalled() + expect(preferencesController.setCurrentPane).not.toHaveBeenCalled() + }) + }) + + describe('Upon Signing In', () => { + it('should open up settings', async () => { + routeService.getRoute = jest.fn().mockReturnValue({ + type: RouteType.Settings, + settingsParams: { + panel: 'general', + }, + } as jest.Mocked) + + await createObserver().handle(ApplicationEvent.SignedIn) + + expect(preferencesController.openPreferences).toHaveBeenCalled() + expect(preferencesController.setCurrentPane).toHaveBeenCalledWith('general') + }) + }) + + describe('Upon Sync Status Changing', () => { + it('should inform the sync controller', async () => { + await createObserver().handle(ApplicationEvent.SyncStatusChanged) + + expect(syncStatusController.update).toHaveBeenCalled() + }) + }) +}) diff --git a/packages/web/src/javascripts/Event/ApplicationEventObserver.ts b/packages/web/src/javascripts/Event/ApplicationEventObserver.ts new file mode 100644 index 000000000..dbe7b42be --- /dev/null +++ b/packages/web/src/javascripts/Event/ApplicationEventObserver.ts @@ -0,0 +1,66 @@ +import { RouteServiceInterface, RouteType } from '@standardnotes/ui-services' +import { ApplicationEvent, SessionsClientInterface, SyncClientInterface } from '@standardnotes/snjs' + +import { PurchaseFlowController } from '@/Controllers/PurchaseFlow/PurchaseFlowController' +import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController' +import { PreferencesController } from '@/Controllers/PreferencesController' +import { SyncStatusController } from '@/Controllers/SyncStatusController' +import { AccountMenuPane } from '@/Components/AccountMenu/AccountMenuPane' + +import { EventObserverInterface } from './EventObserverInterface' + +export class ApplicationEventObserver implements EventObserverInterface { + constructor( + private routeService: RouteServiceInterface, + private purchaseFlowController: PurchaseFlowController, + private accountMenuController: AccountMenuController, + private preferencesController: PreferencesController, + private syncStatusController: SyncStatusController, + private syncClient: SyncClientInterface, + private sessionManager: SessionsClientInterface, + ) {} + + async handle(event: ApplicationEvent): Promise { + switch (event) { + case ApplicationEvent.Launched: + { + const route = this.routeService.getRoute() + switch (route.type) { + case RouteType.Purchase: + this.purchaseFlowController.openPurchaseFlow() + + break + case RouteType.Settings: { + const user = this.sessionManager.getUser() + if (user === undefined) { + this.accountMenuController.setShow(true) + this.accountMenuController.setCurrentPane(AccountMenuPane.SignIn) + + break + } + + this.preferencesController.openPreferences() + this.preferencesController.setCurrentPane(route.settingsParams.panel) + break + } + } + } + break + case ApplicationEvent.SignedIn: + { + const route = this.routeService.getRoute() + switch (route.type) { + case RouteType.Settings: + this.preferencesController.openPreferences() + this.preferencesController.setCurrentPane(route.settingsParams.panel) + + break + } + } + break + case ApplicationEvent.SyncStatusChanged: + this.syncStatusController.update(this.syncClient.getSyncStatus()) + break + } + } +} diff --git a/packages/web/src/javascripts/Event/EventObserverInterface.ts b/packages/web/src/javascripts/Event/EventObserverInterface.ts new file mode 100644 index 000000000..85e25fd0e --- /dev/null +++ b/packages/web/src/javascripts/Event/EventObserverInterface.ts @@ -0,0 +1,5 @@ +import { ApplicationEvent } from '@standardnotes/snjs/dist/@types' + +export interface EventObserverInterface { + handle(event: ApplicationEvent): Promise +}