fix: refactor application event observing on web

This commit is contained in:
Karol Sójko
2022-10-21 14:28:03 +02:00
parent bd40a49484
commit 9fb85cd77a
11 changed files with 238 additions and 47 deletions

View File

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

View File

@@ -139,7 +139,7 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
return isNullOrUndefined(this.apiService.getSession())
}
public getUser() {
public getUser(): Responses.User | undefined {
return this.user
}

View File

@@ -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<Base64String | ClientDisplayableError>
populateSessionFromDemoShareToken(token: Base64String): Promise<void>
getUser(): User | undefined
}

View File

@@ -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<RouteServiceEvent, RouteParserInterface> {
export class RouteService
extends AbstractService<RouteServiceEvent, RouteParserInterface>
implements RouteServiceInterface
{
private unsubApp!: () => void
constructor(

View File

@@ -0,0 +1,7 @@
import { RouteParserInterface } from './RouteParserInterface'
export interface RouteServiceInterface {
deinit(): void
getRoute(): RouteParserInterface
removeSettingsFromURLQueryParameters(): void
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
import { ApplicationEvent } from '@standardnotes/snjs/dist/@types'
export interface EventObserverInterface {
handle(event: ApplicationEvent): Promise<void>
}