fix: refactor application event observing on web
This commit is contained in:
@@ -1,3 +1 @@
|
|||||||
import { Either } from '@standardnotes/common'
|
export type SubscriptionInviteAcceptResponseBody = { success: true } | { success: false; message: string }
|
||||||
|
|
||||||
export type SubscriptionInviteAcceptResponseBody = Either<{ success: true }, { success: false; message: string }>
|
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
|||||||
return isNullOrUndefined(this.apiService.getSession())
|
return isNullOrUndefined(this.apiService.getSession())
|
||||||
}
|
}
|
||||||
|
|
||||||
public getUser() {
|
public getUser(): Responses.User | undefined {
|
||||||
return this.user
|
return this.user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
import { ClientDisplayableError, User } from '@standardnotes/responses'
|
||||||
import { Base64String } from '@standardnotes/sncrypto-common'
|
import { Base64String } from '@standardnotes/sncrypto-common'
|
||||||
|
|
||||||
export interface SessionsClientInterface {
|
export interface SessionsClientInterface {
|
||||||
createDemoShareToken(): Promise<Base64String | ClientDisplayableError>
|
createDemoShareToken(): Promise<Base64String | ClientDisplayableError>
|
||||||
|
|
||||||
populateSessionFromDemoShareToken(token: Base64String): Promise<void>
|
populateSessionFromDemoShareToken(token: Base64String): Promise<void>
|
||||||
|
getUser(): User | undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,16 @@ import {
|
|||||||
ApplicationInterface,
|
ApplicationInterface,
|
||||||
InternalEventBusInterface,
|
InternalEventBusInterface,
|
||||||
} from '@standardnotes/services'
|
} from '@standardnotes/services'
|
||||||
|
|
||||||
import { RouteParser } from './RouteParser'
|
import { RouteParser } from './RouteParser'
|
||||||
import { RouteParserInterface } from './RouteParserInterface'
|
import { RouteParserInterface } from './RouteParserInterface'
|
||||||
import { RouteServiceEvent } from './RouteServiceEvent'
|
import { RouteServiceEvent } from './RouteServiceEvent'
|
||||||
|
import { RouteServiceInterface } from './RouteServiceInterface'
|
||||||
|
|
||||||
export class RouteService extends AbstractService<RouteServiceEvent, RouteParserInterface> {
|
export class RouteService
|
||||||
|
extends AbstractService<RouteServiceEvent, RouteParserInterface>
|
||||||
|
implements RouteServiceInterface
|
||||||
|
{
|
||||||
private unsubApp!: () => void
|
private unsubApp!: () => void
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
7
packages/ui-services/src/Route/RouteServiceInterface.ts
Normal file
7
packages/ui-services/src/Route/RouteServiceInterface.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { RouteParserInterface } from './RouteParserInterface'
|
||||||
|
|
||||||
|
export interface RouteServiceInterface {
|
||||||
|
deinit(): void
|
||||||
|
getRoute(): RouteParserInterface
|
||||||
|
removeSettingsFromURLQueryParameters(): void
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ export * from './Route/Params/SubscriptionInviteParams'
|
|||||||
export * from './Route/RouteParser'
|
export * from './Route/RouteParser'
|
||||||
export * from './Route/RouteType'
|
export * from './Route/RouteType'
|
||||||
export * from './Route/RouteService'
|
export * from './Route/RouteService'
|
||||||
|
export * from './Route/RouteServiceInterface'
|
||||||
export * from './Route/RouteServiceEvent'
|
export * from './Route/RouteServiceEvent'
|
||||||
export * from './Security/AutolockService'
|
export * from './Security/AutolockService'
|
||||||
export * from './Storage/LocalStorage'
|
export * from './Storage/LocalStorage'
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
AutolockService,
|
AutolockService,
|
||||||
IOService,
|
IOService,
|
||||||
RouteService,
|
RouteService,
|
||||||
|
RouteServiceInterface,
|
||||||
ThemeManager,
|
ThemeManager,
|
||||||
WebAlertService,
|
WebAlertService,
|
||||||
} from '@standardnotes/ui-services'
|
} from '@standardnotes/ui-services'
|
||||||
@@ -50,7 +51,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
|||||||
private onVisibilityChange: () => void
|
private onVisibilityChange: () => void
|
||||||
private mobileWebReceiver?: MobileWebReceiver
|
private mobileWebReceiver?: MobileWebReceiver
|
||||||
private androidBackHandler?: AndroidBackHandler
|
private androidBackHandler?: AndroidBackHandler
|
||||||
public readonly routeService: RouteService
|
public readonly routeService: RouteServiceInterface
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
deviceInterface: WebOrDesktopDevice,
|
deviceInterface: WebOrDesktopDevice,
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { PaneController } from './PaneController'
|
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 { WebApplication } from '@/Application/Application'
|
||||||
import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController'
|
import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController'
|
||||||
import { destroyAllObjectProperties } from '@/Utils'
|
import { destroyAllObjectProperties } from '@/Utils'
|
||||||
import {
|
import {
|
||||||
ApplicationEvent,
|
|
||||||
DeinitSource,
|
DeinitSource,
|
||||||
WebOrDesktopDeviceInterface,
|
WebOrDesktopDeviceInterface,
|
||||||
InternalEventBus,
|
InternalEventBus,
|
||||||
@@ -31,10 +30,11 @@ import { NavigationController, NavigationControllerPersistableValue } from './Na
|
|||||||
import { FilePreviewModalController } from './FilePreviewModalController'
|
import { FilePreviewModalController } from './FilePreviewModalController'
|
||||||
import { SelectedItemsController, SelectionControllerPersistableValue } from './SelectedItemsController'
|
import { SelectedItemsController, SelectionControllerPersistableValue } from './SelectedItemsController'
|
||||||
import { HistoryModalController } from './NoteHistory/HistoryModalController'
|
import { HistoryModalController } from './NoteHistory/HistoryModalController'
|
||||||
import { AccountMenuPane } from '@/Components/AccountMenu/AccountMenuPane'
|
|
||||||
import { LinkingController } from './LinkingController'
|
import { LinkingController } from './LinkingController'
|
||||||
import { MasterPersistedValue, PersistenceKey, PersistenceService } from './Abstract/PersistenceService'
|
import { MasterPersistedValue, PersistenceKey, PersistenceService } from './Abstract/PersistenceService'
|
||||||
import { CrossControllerEvent } from './CrossControllerEvent'
|
import { CrossControllerEvent } from './CrossControllerEvent'
|
||||||
|
import { EventObserverInterface } from '@/Event/EventObserverInterface'
|
||||||
|
import { ApplicationEventObserver } from '@/Event/ApplicationEventObserver'
|
||||||
|
|
||||||
export class ViewControllerManager implements InternalEventHandlerInterface {
|
export class ViewControllerManager implements InternalEventHandlerInterface {
|
||||||
readonly enableUnfinishedFeatures: boolean = window?.enabledUnfinishedFeatures
|
readonly enableUnfinishedFeatures: boolean = window?.enabledUnfinishedFeatures
|
||||||
@@ -70,6 +70,7 @@ export class ViewControllerManager implements InternalEventHandlerInterface {
|
|||||||
private itemCounter: ItemCounterInterface
|
private itemCounter: ItemCounterInterface
|
||||||
private subscriptionManager: SubscriptionClientInterface
|
private subscriptionManager: SubscriptionClientInterface
|
||||||
private persistenceService: PersistenceService
|
private persistenceService: PersistenceService
|
||||||
|
private applicationEventObserver: EventObserverInterface
|
||||||
|
|
||||||
constructor(public application: WebApplication, private device: WebOrDesktopDeviceInterface) {
|
constructor(public application: WebApplication, private device: WebOrDesktopDeviceInterface) {
|
||||||
this.eventBus = new InternalEventBus()
|
this.eventBus = new InternalEventBus()
|
||||||
@@ -145,6 +146,16 @@ export class ViewControllerManager implements InternalEventHandlerInterface {
|
|||||||
|
|
||||||
this.historyModalController = new HistoryModalController(this.application, this.eventBus)
|
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()
|
this.addAppEventObserver()
|
||||||
|
|
||||||
if (this.device.appVersion.includes('-beta')) {
|
if (this.device.appVersion.includes('-beta')) {
|
||||||
@@ -240,42 +251,9 @@ export class ViewControllerManager implements InternalEventHandlerInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addAppEventObserver() {
|
addAppEventObserver() {
|
||||||
this.unsubAppEventObserver = this.application.addEventObserver(async (eventName) => {
|
this.unsubAppEventObserver = this.application.addEventObserver(
|
||||||
switch (eventName) {
|
this.applicationEventObserver.handle.bind(this.applicationEventObserver),
|
||||||
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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
persistValues = (): void => {
|
persistValues = (): void => {
|
||||||
|
|||||||
@@ -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<RouteServiceInterface>
|
||||||
|
routeService.getRoute = jest.fn().mockReturnValue({
|
||||||
|
type: RouteType.None,
|
||||||
|
} as jest.Mocked<RouteParserInterface>)
|
||||||
|
|
||||||
|
purchaseFlowController = {} as jest.Mocked<PurchaseFlowController>
|
||||||
|
purchaseFlowController.openPurchaseFlow = jest.fn()
|
||||||
|
|
||||||
|
accountMenuController = {} as jest.Mocked<AccountMenuController>
|
||||||
|
accountMenuController.setShow = jest.fn()
|
||||||
|
accountMenuController.setCurrentPane = jest.fn()
|
||||||
|
|
||||||
|
preferencesController = {} as jest.Mocked<PreferencesController>
|
||||||
|
preferencesController.openPreferences = jest.fn()
|
||||||
|
preferencesController.setCurrentPane = jest.fn()
|
||||||
|
|
||||||
|
syncStatusController = {} as jest.Mocked<SyncStatusController>
|
||||||
|
syncStatusController.update = jest.fn()
|
||||||
|
|
||||||
|
syncClient = {} as jest.Mocked<SyncClientInterface>
|
||||||
|
syncClient.getSyncStatus = jest.fn().mockReturnValue({} as jest.Mocked<SyncOpStatus>)
|
||||||
|
|
||||||
|
sessionManager = {} as jest.Mocked<SessionsClientInterface>
|
||||||
|
sessionManager.getUser = jest.fn().mockReturnValue({} as jest.Mocked<User>)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Upon Application Launched', () => {
|
||||||
|
it('should open up the purchase flow', async () => {
|
||||||
|
routeService.getRoute = jest.fn().mockReturnValue({
|
||||||
|
type: RouteType.Purchase,
|
||||||
|
} as jest.Mocked<RouteParserInterface>)
|
||||||
|
|
||||||
|
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<RouteParserInterface>)
|
||||||
|
|
||||||
|
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<RouteParserInterface>)
|
||||||
|
|
||||||
|
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<RouteParserInterface>)
|
||||||
|
|
||||||
|
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()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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<void> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { ApplicationEvent } from '@standardnotes/snjs/dist/@types'
|
||||||
|
|
||||||
|
export interface EventObserverInterface {
|
||||||
|
handle(event: ApplicationEvent): Promise<void>
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user