import { Injectable } from '@angular/core' import { AlertController, IonicSafeString, ModalController, NavController } from '@ionic/angular' import { wizardModal } from '../components/install-wizard/install-wizard.component' import { WizardBaker } from '../components/install-wizard/prebaked-wizards' import { OSWelcomePage } from '../modals/os-welcome/os-welcome.page' import { S9Server } from '../models/server-model' import { displayEmver } from '../pipes/emver.pipe' import { V1Status } from './api/api-types' import { ApiService, ReqRes } from './api/api.service' import { ConfigService } from './config.service' import { Emver } from './emver.service' import { OsUpdateService } from './os-update.service' @Injectable({ providedIn: 'root' }) export class StartupAlertsNotifier { constructor ( private readonly alertCtrl: AlertController, private readonly navCtrl: NavController, private readonly config: ConfigService, private readonly modalCtrl: ModalController, private readonly apiService: ApiService, private readonly emver: Emver, private readonly osUpdateService: OsUpdateService, private readonly wizardBaker: WizardBaker, ) { const welcome: Check = { name: 'welcome', shouldRun: s => this.shouldRunOsWelcome(s), check: async s => s, display: s => this.displayOsWelcome(s), hasRun: this.config.skipStartupAlerts, } const osUpdate: Check = { name: 'osUpdate', shouldRun: s => this.shouldRunOsUpdateCheck(s), check: s => this.osUpdateCheck(s), display: vl => this.displayOsUpdateCheck(vl), hasRun: this.config.skipStartupAlerts, } const v1StatusUpdate: Check = { name: 'v1Status', shouldRun: s => this.shouldRunOsUpdateCheck(s), check: () => this.v1StatusCheck(), display: s => this.displayV1Check(s), hasRun: this.config.skipStartupAlerts, } const apps: Check = { name: 'apps', shouldRun: s => this.shouldRunAppsCheck(s), check: () => this.appsCheck(), display: () => this.displayAppsCheck(), hasRun: this.config.skipStartupAlerts, } this.checks = [welcome, osUpdate, v1StatusUpdate, apps] } // This takes our three checks and filters down to those that should run. // Then, the reduce fires, quickly iterating through yielding a promise (previousDisplay) to the next element // Each promise fires more or less concurrently, so each c.check(server) is run concurrently // Then, since we await previoudDisplay before c.display(res), each promise executing gets hung awaiting the display of the previous run async runChecks (server: Readonly): Promise { await this.checks .filter(c => !c.hasRun && c.shouldRun(server)) // returning true in the below block means to continue to next modal // returning false means to skip all subsequent modals .reduce(async (previousDisplay, c) => { let checkRes: any try { checkRes = await c.check(server) } catch (e) { console.error(`Exception in ${c.name} check:`, e) return true } c.hasRun = true const displayRes = await previousDisplay if (!checkRes) return true if (displayRes) return c.display(checkRes) }, Promise.resolve(true)) } checks: Check[] private shouldRunOsWelcome (s: S9Server): boolean { return !s.welcomeAck && s.versionInstalled === this.config.version } private shouldRunAppsCheck (server: S9Server): boolean { return server.autoCheckUpdates } private shouldRunOsUpdateCheck (server: S9Server): boolean { return server.autoCheckUpdates } private async v1StatusCheck (): Promise { return this.apiService.checkV1Status() } private async osUpdateCheck (s: Readonly): Promise { const res = await this.apiService.getVersionLatest() return this.osUpdateService.updateIsAvailable(s.versionInstalled, res) ? res : undefined } private async appsCheck (): Promise { const availableApps = await this.apiService.getAvailableApps() return !!availableApps.find( app => app.versionInstalled && this.emver.compare(app.versionInstalled, app.versionLatest) === -1, ) } private async displayOsWelcome (s: Readonly): Promise { return new Promise(async resolve => { const modal = await this.modalCtrl.create({ backdropDismiss: false, component: OSWelcomePage, presentingElement: await this.modalCtrl.getTop(), componentProps: { version: s.versionInstalled, }, }) await modal.present() modal.onWillDismiss().then(res => { return resolve(res.data) }) }) } private async displayOsUpdateCheck (res: ReqRes.GetVersionLatestRes): Promise { const { update } = await this.presentAlertNewOS(res.versionLatest) if (update) { const { cancelled } = await wizardModal( this.modalCtrl, this.wizardBaker.updateOS({ version: res.versionLatest, releaseNotes: res.releaseNotes, }), ) if (cancelled) return true return false } return true } private async displayV1Check (s: V1Status): Promise { return new Promise(async resolve => { if (s.status !== 'available') return resolve(true) const alert = await this.alertCtrl.create({ backdropDismiss: true, header: `EmbassyOS ${s.version} Now Available!`, message: `Version ${s.version} introduces SSD support and a whole lot more.`, buttons: [ { text: 'Cancel', role: 'cancel', handler: () => resolve(true), }, { text: 'View Instructions', handler: () => { window.open(`https://start9.com/eos-${s.version}`, '_blank') resolve(false) }, }, ], }) await alert.present() }) } private async displayAppsCheck (): Promise { return new Promise(async resolve => { const alert = await this.alertCtrl.create({ backdropDismiss: true, header: 'Updates Available!', message: new IonicSafeString( `
New service updates are available in the Marketplace.
You can disable these checks in your Embassy Config
`, ), buttons: [ { text: 'Cancel', role: 'cancel', handler: () => resolve(true), }, { text: 'View in Marketplace', handler: () => { return this.navCtrl.navigateForward('/services/marketplace').then(() => resolve(false)) }, }, ], }) await alert.present() }) } private async presentAlertNewOS (versionLatest: string): Promise<{ cancel?: true, update?: true }> { return new Promise(async resolve => { const alert = await this.alertCtrl.create({ backdropDismiss: true, header: 'New EmbassyOS Version!', message: new IonicSafeString( `
Update EmbassyOS to version ${displayEmver(versionLatest)}?
You can disable these checks in your Embassy Config
`, ), buttons: [ { text: 'Not now', role: 'cancel', handler: () => resolve({ cancel: true }), }, { text: 'Update', handler: () => resolve({ update: true }), }, ], }) await alert.present() }) } } type Check = { // validates whether a check should run based on server properties shouldRun: (s: S9Server) => boolean // executes a check, often requiring api call. It should return a false-y value if there should be no display. check: (s: S9Server) => Promise // display an alert based on the result of the check. // return false if subsequent modals should not be displayed display: (a: T) => Promise // tracks if this check has run in this app instance. hasRun: boolean // for logging purposes name: string }