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