diff --git a/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.module.ts b/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.module.ts index 5233db8d4..7e2f6285f 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.module.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.module.ts @@ -2,7 +2,7 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' import { RouterModule, Routes } from '@angular/router' -import { WifiPage } from './wifi.page' +import { WifiPage, ToWifiIconPipe } from './wifi.page' import { SharedPipesModule } from '@start9labs/shared' const routes: Routes = [ @@ -19,6 +19,6 @@ const routes: Routes = [ RouterModule.forChild(routes), SharedPipesModule, ], - declarations: [WifiPage], + declarations: [WifiPage, ToWifiIconPipe], }) export class WifiPageModule {} diff --git a/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.html b/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.html index 588756b91..625d11264 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.html +++ b/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.html @@ -4,171 +4,156 @@ WiFi Settings - - - Refresh - - - - - - - - -

- Adding WiFi credentials to your StartOS allows you to remove the - Ethernet cable and move the device anywhere you want. StartOS will - automatically connect to available networks. - +
+ + + +

+ Adding WiFi credentials to your Embassy allows you to remove the + Ethernet cable and move the device anywhere you want. Embassy will + automatically connect to available networks. + + View instructions + +

+ + + + Wi-Fi + + + + + + + + + Known Networks + + + + +

{{ ssid.key }}

+

+ + Connected +

+
+
+ + + + +
+ + + + + Connect + + + Forget this network + + + + +
+
+ + Other Networks + + + + {{ avWifi.ssid }} +
+ + Connect + + + +
+
+
+
+
+ - View instructions - -

-
-
- - Country - - - - - - {{ wifi.country }} - {{ this.countries[wifi.country] }} - - Select Country - - - - - Saved Networks - - - - - - - - - - Available Networks - - - - - - - - - - - - - - Saved Networks - - -
- - {{ ssid.key }} - - - - -
- - Available Networks - - - {{ avWifi.ssid }} - - - - - + Other... + + - - - - Join Another Network - - + + + + Known Networks + + + + + + + + Other Networks + + + + + + + +
-
+
diff --git a/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.scss b/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.scss index 8be298f07..376899597 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.scss +++ b/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.scss @@ -1,7 +1,41 @@ +.smaller { + padding: 32px; + max-width: 800px; +} + +.no-padding { + padding-right: 0; +} + .skeleton-parts { ion-button::part(native) { padding-inline-start: 0; padding-inline-end: 0; }; - padding-bottom: 6px; +} + +.connect-button { + font-size: 10px; + font-weight: bold; + margin-right: 12px; +} + +.slot-end { + margin-left: 4px; +} + +ion-item-divider { + text-transform: unset; + padding-bottom: 12px; + padding-left: 0; +} + +ion-item-group { + background-color: #1e2024; + border: 1px solid #717171; + border-radius: 6px; +} + +ion-item { + --background: #1e2024; } \ No newline at end of file diff --git a/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts b/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts index d5821c71e..8921c6078 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts @@ -1,20 +1,30 @@ import { Component } from '@angular/core' -import { - ActionSheetController, - AlertController, - ToastController, -} from '@ionic/angular' -import { AlertInput } from '@ionic/core' +import { ToastController } from '@ionic/angular' import { TuiDialogOptions } from '@taiga-ui/core' +import { ToggleCustomEvent } from '@ionic/core' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ActionSheetButton } from '@ionic/core' -import { ValueSpecObject } from 'start-sdk/lib/config/configTypes' -import { RR } from 'src/app/services/api/api.types' +import { AvailableWifi, RR } from 'src/app/services/api/api.types' import { pauseFor, ErrorToastService } from '@start9labs/shared' -import { ConfigService } from 'src/app/services/config.service' import { FormDialogService } from 'src/app/services/form-dialog.service' import { FormContext, FormPage } from 'src/app/modals/form/form.page' import { LoadingService } from 'src/app/modals/loading/loading.service' +import { PatchDB } from 'patch-db-client' +import { DataModel } from 'src/app/services/patch-db/data-model' +import { ConnectionService } from 'src/app/services/connection.service' +import { Pipe, PipeTransform } from '@angular/core' +import { + BehaviorSubject, + catchError, + distinctUntilChanged, + filter, + from, + merge, + Observable, + Subject, + switchMap, + tap, +} from 'rxjs' +import { wifiSpec } from './wifiSpec' interface WiFiForm { ssid: string @@ -27,101 +37,106 @@ interface WiFiForm { styleUrls: ['wifi.page.scss'], }) export class WifiPage { - loading = true - wifi: RR.GetWifiRes = {} as any - countries = require('../../../util/countries.json') as { - [key: string]: string - } + readonly connected$ = this.connectionService.connected$.pipe(filter(Boolean)) + readonly enabled$ = this.patch.watch$('server-info', 'wifi-enabled').pipe( + distinctUntilChanged(), + tap(enabled => { + if (enabled) this.trigger$.next('') + }), + ) + readonly trigger$ = new BehaviorSubject('') + readonly localChanges$ = new Subject() + readonly wifi$ = merge( + this.trigger$.pipe(switchMap(() => this.getWifi$())), + this.localChanges$, + ) constructor( private readonly api: ApiService, private readonly toastCtrl: ToastController, - private readonly alertCtrl: AlertController, private readonly loader: LoadingService, private readonly formDialog: FormDialogService, private readonly errToast: ErrorToastService, - private readonly actionCtrl: ActionSheetController, - private readonly config: ConfigService, + private readonly patch: PatchDB, + private readonly connectionService: ConnectionService, ) {} - async ngOnInit() { - await this.getWifi() - } + async toggleWifi(e: ToggleCustomEvent): Promise { + const enable = e.detail.checked + const loader = this.loader + .open(enable ? 'Enabling Wifi' : 'Disabling WiFi') + .subscribe() - async getWifi(timeout: number = 0): Promise { - this.loading = true try { - this.wifi = await this.api.getWifi({}, timeout) - if (!this.wifi.country) { - await this.presentAlertCountry() - } + await this.api.enableWifi({ enable }) } catch (e: any) { this.errToast.present(e) } finally { - this.loading = false + loader.unsubscribe() } } - async presentAlertCountry(): Promise { - if (!this.config.isLan) { - const alert = await this.alertCtrl.create({ - header: 'Cannot Complete Action', - message: - 'You must be connected to your server via LAN to change the country.', - buttons: [ - { - text: 'OK', - role: 'cancel', - }, - ], - cssClass: 'enter-click', - }) - await alert.present() - return + async connect(ssid: string): Promise { + const loader = this.loader + .open('Connecting. This could take a while...') + .subscribe() + + try { + await this.api.connectWifi({ ssid }) + await this.confirmWifi(ssid) + } catch (e: any) { + this.errToast.present(e) + } finally { + loader.unsubscribe() } - - const inputs: AlertInput[] = Object.entries(this.countries).map( - ([country, fullName]) => { - return { - name: fullName, - type: 'radio', - label: `${country} - ${fullName}`, - value: country, - checked: country === this.wifi.country, - } - }, - ) - - const alert = await this.alertCtrl.create({ - header: 'Select Country', - subHeader: - 'Warning: Changing the country will delete all saved networks from StartOS.', - inputs, - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: 'Save', - handler: (country: string) => this.setCountry(country), - }, - ], - cssClass: 'enter-click select-warning', - }) - await alert.present() } - presentModalAdd(ssid?: string, needsPW: boolean = true) { - const { name, spec } = getWifiValueSpec(ssid, needsPW) + async forget(ssid: string, wifi: RR.GetWifiRes): Promise { + const loader = this.loader.open('Deleting...').subscribe() + + try { + await this.api.deleteWifi({ ssid }) + delete wifi.ssids[ssid] + this.localChanges$.next(wifi) + this.trigger$.next('') + } catch (e: any) { + this.errToast.present(e) + } finally { + loader.unsubscribe() + } + } + + async presentModalAdd(network: AvailableWifi) { + if (!network.security.length) { + this.connect(network.ssid) + } else { + const options: Partial>> = { + label: 'Password Needed', + data: { + spec: wifiSpec.spec, + buttons: [ + { + text: 'Connect', + handler: async ({ ssid, password }) => + this.saveAndConnect(ssid, password), + }, + ], + }, + } + this.formDialog.open(FormPage, options) + } + } + + presentModalAddOther(wifi: RR.GetWifiRes) { const options: Partial>> = { - label: name, + label: wifiSpec.name, data: { - spec, + spec: wifiSpec.spec, buttons: [ { text: 'Save for Later', - handler: async ({ ssid, password }) => this.save(ssid, password), + handler: async ({ ssid, password }) => + this.save(ssid, password, wifi), }, { text: 'Save and Connect', @@ -131,107 +146,36 @@ export class WifiPage { ], }, } - this.formDialog.open(FormPage, options) } - async presentAction(ssid: string) { - const buttons: ActionSheetButton[] = [ - { - text: 'Forget', - icon: 'trash', - role: 'destructive', - handler: () => { - this.delete(ssid) - }, - }, - ] - - if (ssid !== this.wifi.connected) { - buttons.unshift({ - text: 'Connect', - icon: 'wifi', - handler: () => { - this.connect(ssid) - }, - }) - } - - const action = await this.actionCtrl.create({ - header: ssid, - subHeader: 'Manage network', - mode: 'ios', - buttons, - }) - - await action.present() + private getWifi$(): Observable { + return from(this.api.getWifi({}, 10000)).pipe( + catchError((e: any) => { + this.errToast.present(e) + return [] + }), + ) } - private async setCountry(country: string): Promise { - const loader = this.loader.open('Setting country...').subscribe() - - try { - await this.api.setWifiCountry({ country }) - await this.getWifi() - this.wifi.country = country - } catch (e: any) { - this.errToast.present(e) - } finally { - loader.unsubscribe() - } - } - - private async confirmWifi( - ssid: string, - deleteOnFailure = false, - ): Promise { - const maxAttempts = 5 - let attempts = 0 - - while (attempts < maxAttempts) { - if (attempts > maxAttempts) { - this.presentToastFail() - if (deleteOnFailure) { - delete this.wifi.ssids[ssid] - } - break - } - - try { - const start = new Date().valueOf() - await this.getWifi() - const end = new Date().valueOf() - if (this.wifi.connected === ssid) { - this.presentAlertSuccess(ssid) - break - } else { - attempts++ - const diff = end - start - // depending on the response time, wait a min of 1000 ms, and a max of 4000 ms in between retries. Both 1000 and 4000 are arbitrary - await pauseFor(Math.max(1000, 4000 - diff)) - } - } catch (e) { - attempts++ - console.warn(e) - } - } - } - - private async presentAlertSuccess(ssid: string): Promise { - const alert = await this.alertCtrl.create({ - header: `Connected to "${ssid}"`, - message: - 'Note. It may take several minutes to an hour for StartOS to reconnect over Tor.', + private async presentToastSuccess(): Promise { + const toast = await this.toastCtrl.create({ + header: 'Connection successful!', + position: 'bottom', + duration: 4000, buttons: [ { - text: 'Ok', - role: 'cancel', - cssClass: 'enter-click', + side: 'start', + icon: 'close', + handler: () => { + return true + }, }, ], + cssClass: 'success-toast', }) - await alert.present() + await toast.present() } private async presentToastFail(): Promise { @@ -255,36 +199,11 @@ export class WifiPage { await toast.present() } - private async connect(ssid: string): Promise { - const loader = this.loader - .open('Connecting. This could take a while...') - .subscribe() - - try { - await this.api.connectWifi({ ssid }) - await this.confirmWifi(ssid) - } catch (e: any) { - this.errToast.present(e) - } finally { - loader.unsubscribe() - } - } - - private async delete(ssid: string): Promise { - const loader = this.loader.open('Deleting...').subscribe() - - try { - await this.api.deleteWifi({ ssid }) - await this.getWifi() - delete this.wifi.ssids[ssid] - } catch (e: any) { - this.errToast.present(e) - } finally { - loader.unsubscribe() - } - } - - private async save(ssid: string, password: string): Promise { + private async save( + ssid: string, + password: string, + wifi: RR.GetWifiRes, + ): Promise { const loader = this.loader.open('Saving...').subscribe() try { @@ -294,7 +213,9 @@ export class WifiPage { priority: 0, connect: false, }) - await this.getWifi() + wifi.ssids[ssid] = 0 + this.localChanges$.next(wifi) + this.trigger$.next('') return true } catch (e: any) { this.errToast.present(e) @@ -319,7 +240,7 @@ export class WifiPage { priority: 0, connect: true, }) - await this.confirmWifi(ssid, true) + await this.confirmWifi(ssid) return true } catch (e: any) { this.errToast.present(e) @@ -328,52 +249,52 @@ export class WifiPage { loader.unsubscribe() } } -} -function getWifiValueSpec( - ssid?: string, - needsPW: boolean = true, -): ValueSpecObject { - return { - type: 'object', - name: 'WiFi Credentials', - description: - 'Enter the network SSID and password. You can connect now or save the network for later.', - warning: null, - spec: { - ssid: { - type: 'text', - name: 'Network SSID', - description: null, - inputmode: 'text', - placeholder: null, - patterns: [], - minLength: null, - maxLength: null, - required: true, - masked: false, - default: ssid || null, - warning: null, - }, - password: { - type: 'text', - name: 'Password', - description: null, - inputmode: 'text', - placeholder: null, - required: needsPW, - masked: true, - minLength: null, - maxLength: null, - patterns: [ - { - regex: '^.{8,}$', - description: 'Must be longer than 8 characters', - }, - ], - default: null, - warning: null, - }, - }, + private async confirmWifi(ssid: string): Promise { + const maxAttempts = 5 + let attempts = 0 + + while (true) { + if (attempts > maxAttempts) { + this.presentToastFail() + break + } + + try { + const start = new Date().valueOf() + const newWifi = await this.api.getWifi({}, 10000) + const end = new Date().valueOf() + if (newWifi.connected === ssid) { + this.localChanges$.next(newWifi) + this.presentToastSuccess() + break + } else { + attempts++ + const diff = end - start + // depending on the response time, wait a min of 1000 ms, and a max of 4000 ms in between retries. Both 1000 and 4000 are arbitrary + await pauseFor(Math.max(1000, 4000 - diff)) + } + } catch (e) { + attempts++ + console.warn(e) + } + } + } +} + +@Pipe({ + name: 'toWifiIcon', +}) +export class ToWifiIconPipe implements PipeTransform { + transform(signal: number): string { + if (signal >= 0 && signal < 5) { + return 'assets/img/icons/wifi-0.png' + } else if (signal >= 5 && signal < 50) { + return 'assets/img/icons/wifi-1.png' + } else if (signal >= 50 && signal < 90) { + return 'assets/img/icons/wifi-2.png' + } else { + return 'assets/img/icons/wifi-3.png' + } } } diff --git a/frontend/projects/ui/src/app/pages/server-routes/wifi/wifiSpec.ts b/frontend/projects/ui/src/app/pages/server-routes/wifi/wifiSpec.ts new file mode 100644 index 000000000..5945a6bfb --- /dev/null +++ b/frontend/projects/ui/src/app/pages/server-routes/wifi/wifiSpec.ts @@ -0,0 +1,44 @@ +import { ValueSpecObject } from 'start-sdk/lib/config/configTypes' + +export const wifiSpec: ValueSpecObject = { + type: 'object', + name: 'WiFi Credentials', + description: + 'Enter the network SSID and password. You can connect now or save the network for later.', + warning: null, + spec: { + ssid: { + type: 'text', + minLength: null, + maxLength: null, + patterns: [], + name: 'Network SSID', + description: null, + inputmode: 'text', + placeholder: null, + required: true, + masked: false, + default: null, + warning: null, + }, + password: { + type: 'text', + minLength: null, + maxLength: null, + patterns: [ + { + regex: '^.{8,}$', + description: 'Must be longer than 8 characters', + }, + ], + name: 'Password', + description: null, + inputmode: 'text', + placeholder: null, + required: true, + masked: true, + default: null, + warning: null, + }, + }, +} diff --git a/frontend/projects/ui/src/app/services/api/api.types.ts b/frontend/projects/ui/src/app/services/api/api.types.ts index 87418d4e0..28e781c21 100644 --- a/frontend/projects/ui/src/app/services/api/api.types.ts +++ b/frontend/projects/ui/src/app/services/api/api.types.ts @@ -104,9 +104,6 @@ export module RR { // wifi - export type SetWifiCountryReq = { country: string } - export type SetWifiCountryRes = null - export type GetWifiReq = {} export type GetWifiRes = { ssids: { @@ -127,6 +124,9 @@ export module RR { } export type AddWifiRes = null + export type EnableWifiReq = { enable: boolean } // wifi.enable + export type EnableWifiRes = null + export type ConnectWifiReq = { ssid: string } // wifi.connect export type ConnectWifiRes = null diff --git a/frontend/projects/ui/src/app/services/api/embassy-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-api.service.ts index c11324eaf..877c442ae 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-api.service.ts @@ -147,15 +147,13 @@ export abstract class ApiService { // wifi + abstract enableWifi(params: RR.EnableWifiReq): Promise + abstract getWifi( params: RR.GetWifiReq, timeout: number, ): Promise - abstract setWifiCountry( - params: RR.SetWifiCountryReq, - ): Promise - abstract addWifi(params: RR.AddWifiReq): Promise abstract connectWifi(params: RR.ConnectWifiReq): Promise diff --git a/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts index 73007227b..8d958dc18 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts @@ -266,6 +266,10 @@ export class LiveApiService extends ApiService { // wifi + async enableWifi(params: RR.EnableWifiReq): Promise { + return this.rpcRequest({ method: 'wifi.enable', params }) + } + async getWifi( params: RR.GetWifiReq, timeout?: number, @@ -273,12 +277,6 @@ export class LiveApiService extends ApiService { return this.rpcRequest({ method: 'wifi.get', params, timeout }) } - async setWifiCountry( - params: RR.SetWifiCountryReq, - ): Promise { - return this.rpcRequest({ method: 'wifi.country.set', params }) - } - async addWifi(params: RR.AddWifiReq): Promise { return this.rpcRequest({ method: 'wifi.add', params }) } diff --git a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts index 65c13aa03..891fcc987 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -418,18 +418,23 @@ export class MockApiService extends ApiService { // wifi + async enableWifi(params: RR.EnableWifiReq): Promise { + await pauseFor(2000) + const patch = [ + { + op: PatchOp.REPLACE, + path: '/server-info/wifi-enabled', + value: params.enable, + }, + ] + return this.withRevision(patch, null) + } + async getWifi(params: RR.GetWifiReq): Promise { await pauseFor(2000) return Mock.Wifi } - async setWifiCountry( - params: RR.SetWifiCountryReq, - ): Promise { - await pauseFor(2000) - return null - } - async addWifi(params: RR.AddWifiReq): Promise { await pauseFor(2000) return null diff --git a/frontend/projects/ui/src/app/services/api/mock-patch.ts b/frontend/projects/ui/src/app/services/api/mock-patch.ts index dc1139780..97246ee5d 100644 --- a/frontend/projects/ui/src/app/services/api/mock-patch.ts +++ b/frontend/projects/ui/src/app/services/api/mock-patch.ts @@ -41,7 +41,8 @@ export const mockPatchData: DataModel = { }, 'server-info': { id: 'abcdefgh', - version: '0.3.4.3', + version: '0.3.4', + country: 'us', 'last-backup': new Date(new Date().valueOf() - 604800001).toISOString(), 'lan-address': 'https://adjective-noun.local', 'tor-address': 'http://myveryownspecialtoraddress.onion', @@ -56,6 +57,7 @@ export const mockPatchData: DataModel = { }, }, 'last-wifi-region': null, + 'wifi-enabled': false, 'unread-notification-count': 4, // password is asdfasdf 'password-hash': diff --git a/frontend/projects/ui/src/app/services/patch-db/data-model.ts b/frontend/projects/ui/src/app/services/patch-db/data-model.ts index d6309eac5..598890b0b 100644 --- a/frontend/projects/ui/src/app/services/patch-db/data-model.ts +++ b/frontend/projects/ui/src/app/services/patch-db/data-model.ts @@ -64,11 +64,13 @@ export interface DevProjectData { export interface ServerInfo { id: string version: string + country: string 'last-backup': string | null 'lan-address': Url 'tor-address': Url 'ip-info': IpInfo 'last-wifi-region': string | null + 'wifi-enabled': boolean 'unread-notification-count': number 'status-info': ServerStatusInfo 'eos-version-compat': string