From 988f5fb1faa839c9fe05e59fe9d1a20a64b57038 Mon Sep 17 00:00:00 2001 From: "Leonardo \"Leu\" Pereira" Date: Fri, 25 Oct 2024 11:32:55 -0300 Subject: [PATCH] Clean up home.page.ts (#21) * centralize loading overlay logic * re-enable console log easter-egg * revert unwanted environment.ts change * fix migration service * remove bloat popover logic * lint refactor * create password service for home page * Refactor home page code to remove unused event listeners * refactor countdown timer precision using rxjs * Reorganize components to reside in its page's folders * Refactor home page code to remove unused event listeners * moved add account modal logic to its component * remove unused service injection * Refactor account modal component to better handle camera status * Refactor account modal component to enable closing when scan is not active and avoid being stuck * fixes #22 --- .eslintrc.json | 3 +- package.json | 2 +- src/app/app.component.ts | 2 +- .../countdown-timer.component.ts | 59 -- .../account-detail.component.html | 0 .../account-detail.component.scss | 0 .../account-detail.component.ts | 8 +- .../account-list/account-list.component.html | 0 .../account-list/account-list.component.scss | 0 .../account-list/account-list.component.ts | 0 .../account-modal.component.html | 86 +++ .../account-modal.component.scss | 20 + .../account-modal/account-modal.component.ts | 295 +++++++++ .../account-select-modal.component.html | 0 .../account-select-modal.component.scss | 0 .../account-select-modal.component.ts | 0 .../countdown-timer.component.html | 0 .../countdown-timer.component.scss | 0 .../countdown-timer.component.ts | 56 ++ src/app/home/home.module.ts | 11 +- src/app/home/home.page.html | 102 +-- src/app/home/home.page.scss | 20 - src/app/home/home.page.ts | 583 +++--------------- src/app/home/password.service.ts | 230 +++++++ src/app/login/login.page.scss | 2 +- src/app/login/login.page.ts | 2 +- src/app/models/account2FA.model.ts | 10 +- src/app/models/app-version.enum.ts | 7 + src/app/models/brand-fetch-search.model.ts | 5 +- .../accounts/remote-account2fa.service.ts | 6 +- src/app/services/app-config.service.ts | 3 +- src/app/services/authentication.service.ts | 1 - src/app/services/logging.service.ts | 31 +- src/app/services/logo.service.ts | 4 +- src/app/services/migration.service.ts | 7 +- src/app/utils/crypto-utils.ts | 2 +- src/app/utils/global-utils.ts | 24 +- src/app/utils/version-utils.ts | 13 +- src/assets/i18n/en.json | 3 +- src/assets/i18n/pt.json | 3 +- src/environments/environment.ts | 2 +- 41 files changed, 883 insertions(+), 719 deletions(-) delete mode 100644 src/app/components/countdown-timer/countdown-timer.component.ts rename src/app/{ => home}/components/account-detail/account-detail.component.html (100%) rename src/app/{ => home}/components/account-detail/account-detail.component.scss (100%) rename src/app/{ => home}/components/account-detail/account-detail.component.ts (93%) rename src/app/{ => home}/components/account-list/account-list.component.html (100%) rename src/app/{ => home}/components/account-list/account-list.component.scss (100%) rename src/app/{ => home}/components/account-list/account-list.component.ts (100%) create mode 100644 src/app/home/components/account-modal/account-modal.component.html create mode 100644 src/app/home/components/account-modal/account-modal.component.scss create mode 100644 src/app/home/components/account-modal/account-modal.component.ts rename src/app/{ => home}/components/account-select-modal/account-select-modal.component.html (100%) rename src/app/{ => home}/components/account-select-modal/account-select-modal.component.scss (100%) rename src/app/{ => home}/components/account-select-modal/account-select-modal.component.ts (100%) rename src/app/{ => home}/components/countdown-timer/countdown-timer.component.html (100%) rename src/app/{ => home}/components/countdown-timer/countdown-timer.component.scss (100%) create mode 100644 src/app/home/components/countdown-timer/countdown-timer.component.ts create mode 100644 src/app/home/password.service.ts diff --git a/.eslintrc.json b/.eslintrc.json index eaf1bce..e494a5d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -13,7 +13,8 @@ }, "extends": [ "plugin:@angular-eslint/recommended", - "plugin:@angular-eslint/template/process-inline-templates" + "plugin:@angular-eslint/template/process-inline-templates", + "plugin:@typescript-eslint/recommended" ], "rules": { "@angular-eslint/component-class-suffix": [ diff --git a/package.json b/package.json index db989af..5fc181b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "authleu", - "version": "2.1.0", + "version": "2.1.1", "author": "Leonardo 'Leu' Pereira ", "homepage": "https://github.com/jlcvp/AuthLeu", "description": "Open source authenticator and 2fa code generator to use across multiple devices and platforms", diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 9913a61..617685b 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -61,7 +61,7 @@ export class AppComponent { this.translate.setDefaultLang('en') const supportedLanguages = /en|pt/ // get the browser language - let browserLang = this.translate.getBrowserLang() + const browserLang = this.translate.getBrowserLang() console.log("detected browser language: ", browserLang) if (browserLang !== undefined && this.translate.getBrowserLang()?.match(supportedLanguages)) { this.translate.use(browserLang) diff --git a/src/app/components/countdown-timer/countdown-timer.component.ts b/src/app/components/countdown-timer/countdown-timer.component.ts deleted file mode 100644 index a9757a6..0000000 --- a/src/app/components/countdown-timer/countdown-timer.component.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; - -@Component({ - selector: 'app-countdown-timer', - templateUrl: './countdown-timer.component.html', - styleUrls: ['./countdown-timer.component.scss'], -}) -export class CountdownTimerComponent { - private timerRefreshInterval: any - private _timerStartTime = 0 - private _seconds = 0 - @Input() set seconds(value: number) { - this._seconds = value - this.timerLabel = value - this.stopTimer() - } - get seconds(): number { - return this._seconds - } - - @Output() timerEnd = new EventEmitter(); - - timerLabel: number = 0; - - constructor() { } - - public startTimer() { - this.setupTimerInterval() - } - - private stopTimer() { - if(this.timerRefreshInterval) { - clearInterval(this.timerRefreshInterval) - } - } - - private setupTimerInterval() { - if(this.timerRefreshInterval) { - clearInterval(this.timerRefreshInterval) - } - - this.timerLabel = this.seconds // reset timer label - this._timerStartTime = Date.now() // reset timer start time - - this.timerRefreshInterval = setInterval(() => { - this.updateTimerLabel() - if(this.timerLabel <= 0) { - clearInterval(this.timerRefreshInterval) - this.timerEnd.emit() - } - }, 100) // for precision purposes check every 250ms - } - - private updateTimerLabel() { - const elapsedTime = Math.ceil((Date.now() - this._timerStartTime)/1000) - this.timerLabel = this.seconds - elapsedTime - } - -} diff --git a/src/app/components/account-detail/account-detail.component.html b/src/app/home/components/account-detail/account-detail.component.html similarity index 100% rename from src/app/components/account-detail/account-detail.component.html rename to src/app/home/components/account-detail/account-detail.component.html diff --git a/src/app/components/account-detail/account-detail.component.scss b/src/app/home/components/account-detail/account-detail.component.scss similarity index 100% rename from src/app/components/account-detail/account-detail.component.scss rename to src/app/home/components/account-detail/account-detail.component.scss diff --git a/src/app/components/account-detail/account-detail.component.ts b/src/app/home/components/account-detail/account-detail.component.ts similarity index 93% rename from src/app/components/account-detail/account-detail.component.ts rename to src/app/home/components/account-detail/account-detail.component.ts index f2bd684..af19749 100644 --- a/src/app/components/account-detail/account-detail.component.ts +++ b/src/app/home/components/account-detail/account-detail.component.ts @@ -3,7 +3,7 @@ import { ToastController } from '@ionic/angular'; import { Account2FA } from 'src/app/models/account2FA.model'; import { OtpService } from 'src/app/services/otp.service'; import { CountdownTimerComponent } from '../countdown-timer/countdown-timer.component'; -import { debounceTime, firstValueFrom, pipe } from 'rxjs'; +import { firstValueFrom } from 'rxjs'; import { TranslateService } from '@ngx-translate/core'; @Component({ @@ -26,6 +26,7 @@ export class AccountDetailComponent { @HostListener('window:focus', ['$event']) onFocus(event: FocusEvent): void { // resume timer + console.log('focus event', event) this.updateCode() this.updateTokenCountdown() } @@ -76,10 +77,11 @@ export class AccountDetailComponent { return this._tokenCountdown } - async copyCode(evt: any) { + async copyCode(evt: any) { // eslint-disable-line @typescript-eslint/no-explicit-any if(!this.account) { return } + console.log("copying code", {evt}) const code = this.token.replace(/\s/g, '') await navigator.clipboard.writeText(code) console.log("code copied") @@ -100,7 +102,7 @@ export class AccountDetailComponent { setTimeout(() => { this.updateTokenCountdown() this.updateCode() - }, 500); + }, 150); // using 150ms to debounce the timer end event } updateCode() { diff --git a/src/app/components/account-list/account-list.component.html b/src/app/home/components/account-list/account-list.component.html similarity index 100% rename from src/app/components/account-list/account-list.component.html rename to src/app/home/components/account-list/account-list.component.html diff --git a/src/app/components/account-list/account-list.component.scss b/src/app/home/components/account-list/account-list.component.scss similarity index 100% rename from src/app/components/account-list/account-list.component.scss rename to src/app/home/components/account-list/account-list.component.scss diff --git a/src/app/components/account-list/account-list.component.ts b/src/app/home/components/account-list/account-list.component.ts similarity index 100% rename from src/app/components/account-list/account-list.component.ts rename to src/app/home/components/account-list/account-list.component.ts diff --git a/src/app/home/components/account-modal/account-modal.component.html b/src/app/home/components/account-modal/account-modal.component.html new file mode 100644 index 0000000..ebb6663 --- /dev/null +++ b/src/app/home/components/account-modal/account-modal.component.html @@ -0,0 +1,86 @@ + + + {{ "ADD_ACCOUNT_MODAL.TITLE" | translate }} + + + + + + + + + + + + + + + + + + + + + + + + {{ "ADD_ACCOUNT_MODAL.MANUAL_INPUT" | translate }} + + + + + + + + + {{ "ADD_ACCOUNT_MODAL.SCAN_QR_CODE" | translate }} + +
+ + + + + + + + + + + + + + {{ "ADD_ACCOUNT_MODAL.LOGO" | translate }} + + + + + + +

{{ "ADD_ACCOUNT_MODAL.NO_LOGO" | translate }}

+
+
+ + + + + +
+
+

Logos provided by Brandfetch +

+
+ {{ "ADD_ACCOUNT_MODAL.SAVE" | + translate }} +
+
+
\ No newline at end of file diff --git a/src/app/home/components/account-modal/account-modal.component.scss b/src/app/home/components/account-modal/account-modal.component.scss new file mode 100644 index 0000000..46e0de5 --- /dev/null +++ b/src/app/home/components/account-modal/account-modal.component.scss @@ -0,0 +1,20 @@ +.full-row { + width: 100%; +} + +.selected-logo { + border: 3px solid var(--ion-color-danger); +} + +.logo-img { + background-color: whitesmoke; + box-shadow: 0 0 3px 0 rgba(0, 0, 0, 0.2); +} + +.logo-card { + aspect-ratio: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} diff --git a/src/app/home/components/account-modal/account-modal.component.ts b/src/app/home/components/account-modal/account-modal.component.ts new file mode 100644 index 0000000..f207d18 --- /dev/null +++ b/src/app/home/components/account-modal/account-modal.component.ts @@ -0,0 +1,295 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { FormGroup, FormControl, Validators, FormBuilder } from '@angular/forms'; +import { AlertController, LoadingController, ModalController, SearchbarCustomEvent, ToastController } from '@ionic/angular'; +import { TranslateService } from '@ngx-translate/core'; +import { NgxScannerQrcodeComponent, ScannerQRCodeConfig, ScannerQRCodeResult } from 'ngx-scanner-qrcode'; +import { firstValueFrom } from 'rxjs'; +import { Account2FA } from 'src/app/models/account2FA.model'; +import { LogoService } from 'src/app/services/logo.service'; + +@Component({ + selector: 'app-account-modal', + templateUrl: './account-modal.component.html', + styleUrls: ['./account-modal.component.scss'], +}) +export class AccountModalComponent implements OnInit { + @ViewChild('qrscanner') qrscanner!: NgxScannerQrcodeComponent; + + isScanActive = false; + isCameraSettled = false; + draftLogoURL = ''; + draftLogoSearchTxt = ''; + searchLogoResults: string[] = []; + + qrScannerOpts: ScannerQRCodeConfig = { + isBeep: false, + vibrate: 100, + constraints: { + video: { + facingMode: 'environment' + } + } + } + validations_form: FormGroup; + + private loading: HTMLIonLoadingElement | undefined = undefined + + constructor( + formBuilder: FormBuilder, + private translateService: TranslateService, + private logoService: LogoService, + private toastController: ToastController, + private loadingController: LoadingController, + private alertController: AlertController, + private modalController: ModalController + ) { + this.validations_form = formBuilder.group({ + label: new FormControl('', Validators.compose([ + Validators.required, + ])), + secret: new FormControl('', Validators.compose([ + Validators.required, + Validators.minLength(8), + Validators.pattern('^[A-Z2-7]+=*$') + ])), + tokenLength: new FormControl(6, Validators.compose([ + Validators.required, + Validators.pattern('^[1-9]+[0-9]*$') + ])), + interval: new FormControl(30, Validators.compose([ + Validators.required, + Validators.pattern('^[1-9]+[0-9]*$') + ])), + }); + } + + ngOnInit(): void { + this.scanCode() + } + + async onWillDismiss() { + if (this.qrscanner) { + console.log("STOP QR") + await this.stopScanner() + } + } + + async dismiss(data: Account2FA | null = null) { + // Dismiss the modal + const role = data ? 'added' : 'cancel' + await this.onWillDismiss() + await this.modalController.dismiss(data, role) + } + + async onQRCodeScanned(evt: ScannerQRCodeResult[], qrscanner: NgxScannerQrcodeComponent) { + try { + await qrscanner.stop() + await this.stopScanner() + } catch (error) { + console.error("Error stopping scanner", error) + } + this.processQRCode(evt && evt[0]?.value || '') + // give time for the scanner to stop + setTimeout(() => { + this.isScanActive = false + }, 200); + } + + async cycleCamera() { + this.isCameraSettled = false + const current = this.qrscanner.deviceIndexActive + const devices = await firstValueFrom(this.qrscanner.devices) + const next = (current + 1) % devices.length + const nextDevice = devices[next] + console.log(`cycle device [${current}] -> [${next}]`, { playDevice: nextDevice, devices }) + await this.qrscanner.playDevice(nextDevice.deviceId) + await this.showInfoToast(`${nextDevice.label}`, 'top') + await this.awaitCameraSettle() + this.isCameraSettled = true + } + + async manualInputAction() { + if (this.qrscanner) { + console.log("STOP QR Reading") + await this.stopScanner() + } + this.isScanActive = false; + } + + async scanCode() { + // + this.isScanActive = true + const message = await firstValueFrom(this.translateService.get('ADD_ACCOUNT_MODAL.LOADING_CAMERA')) + await this.presentLoading(message) + try { + await firstValueFrom(this.qrscanner.start()) + console.log('QR scanner started') + await this.awaitCameraSettle() + } catch (error) { + // camera permission denial + const header = await firstValueFrom(this.translateService.get('ADD_ACCOUNT_MODAL.ERROR_MSGS.ERROR_CAMERA_HEADER')) + const message = await firstValueFrom(this.translateService.get('ADD_ACCOUNT_MODAL.ERROR_MSGS.ERROR_CAMERA_MESSAGE')) + await this.dismissLoading() + await this.showAlert(message, header) + this.isScanActive = false + return + } + + const devices = (await firstValueFrom(this.qrscanner.devices)).filter(device => device.kind === 'videoinput') + console.log({ devices }) + // find back camera + let backCamera = devices.find(device => device.label.toLowerCase().includes('back camera')) + if (!backCamera) { + backCamera = devices.find(device => device.label.toLowerCase().match(/.*back.*camera.*/)) + } + + if (backCamera && backCamera.deviceId) { + console.log("using device", { backCamera }) + await this.qrscanner.playDevice(backCamera.deviceId) + await this.awaitCameraSettle() + } + this.isCameraSettled = true + await this.dismissLoading() + } + + async handleSearchLogo(evt: SearchbarCustomEvent) { + const searchTerm = evt?.detail?.value + console.log({ evt, searchTerm }) + if (!searchTerm) { + this.draftLogoURL = '' + this.searchLogoResults = [] + return + } + const brandInfo = await this.logoService.searchServiceInfo(searchTerm) + if (brandInfo && brandInfo.length > 0) { + this.draftLogoURL = brandInfo[0].logo + this.searchLogoResults = brandInfo.map(brand => brand.logo) + } + console.log({ brandInfo, draftLogoURL: this.draftLogoURL, searchTerm: this.draftLogoSearchTxt, results: this.searchLogoResults }) + } + + selectLogo(logoURL: string) { + this.draftLogoURL = logoURL + } + + async createAccount(formValues: any) { // eslint-disable-line @typescript-eslint/no-explicit-any + console.log({ formValues }) + const logo = this.draftLogoURL + const newAccountDict = Object.assign(formValues, { logo, active: true }) + try { + const account = Account2FA.fromDictionary(newAccountDict) + console.log({ account2fa: account }) + this.dismiss(account) + } catch (error: any) { // eslint-disable-line @typescript-eslint/no-explicit-any + const message = await firstValueFrom(this.translateService.get('ADD_ACCOUNT_MODAL.ERROR_MSGS.INVALID_FIELDS')) + console.error("Error adding account", error) + await this.showErrorToast(message) + } + } + + private async processQRCode(evt: string) { + // The URI format and params is described in https://github.com/google/google-authenticator/wiki/Key-Uri-Format + // otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30 + try { + const account = Account2FA.fromOTPAuthURL(evt) + console.log({ account }) + + this.validations_form.controls['label'].setValue(account.label) + this.validations_form.controls['secret'].setValue(account.secret) + this.validations_form.controls['tokenLength'].setValue(account.tokenLength) + this.validations_form.controls['interval'].setValue(account.interval) + // service name inferred from issuer or label + const serviceName = account.issuer || account.label.split(':')[0] + const event = new CustomEvent('search', { detail: { value: serviceName } }) as SearchbarCustomEvent + this.handleSearchLogo(event) + } catch (error) { + const message = await firstValueFrom(this.translateService.get('ADD_ACCOUNT_MODAL.ERROR_MSGS.INVALID_QR_CODE')) + console.error("Error processing QR code", error) + await this.showErrorToast(message) + } + } + + private async showInfoToast(message: string, position: "top" | "bottom" | "middle" | undefined = undefined) { + const toast = await this.toastController.create({ + message, + duration: 2000, + position: position + }) + await toast.present() + } + + private async showErrorToast(message: string) { + const toast = await this.toastController.create({ + message, + duration: 2000, + color: 'danger', + position: 'middle' + }) + await toast.present() + } + + private async presentLoading(message: string): Promise { + // if loading is already present, update message + if(this.loading != undefined) { + this.loading.message = message + return + } + + // create new loading + this.loading = await this.loadingController.create({ + message, + backdropDismiss: false + }) + await this.loading.present() + } + + private async dismissLoading(): Promise { + await this.loading?.dismiss() + this.loading = undefined + } + + private async showAlert(message: string, header: string) { + const alert = await this.alertController.create({ + header, + message, + buttons: ['OK'] + }) + await alert.present() + } + + private async stopScanner() { + if(this.qrscanner) { + await this.awaitCameraSettle() + await firstValueFrom(this.qrscanner.stop()) + } + } + + private async awaitCameraSettle(timeoutMillis: number = 10000) { + return new Promise((resolve, reject) => { + console.log("Waiting for camera to settle") + if(!this.qrscanner) { + reject("No scanner") + return + } + let timeout: any = undefined // eslint-disable-line @typescript-eslint/no-explicit-any + // while not settled, keep checking every 100ms + const interval = setInterval(() => { + if(this.qrscanner.isStart) { + clearInterval(interval) + if(timeout) { + clearTimeout(timeout) + } + console.log("Camera settled") + resolve() + } + }, 100) + + // if timeout, reject + timeout = setTimeout(() => { + clearInterval(interval) + reject("Timeout") + }, timeoutMillis) + + }) + } +} diff --git a/src/app/components/account-select-modal/account-select-modal.component.html b/src/app/home/components/account-select-modal/account-select-modal.component.html similarity index 100% rename from src/app/components/account-select-modal/account-select-modal.component.html rename to src/app/home/components/account-select-modal/account-select-modal.component.html diff --git a/src/app/components/account-select-modal/account-select-modal.component.scss b/src/app/home/components/account-select-modal/account-select-modal.component.scss similarity index 100% rename from src/app/components/account-select-modal/account-select-modal.component.scss rename to src/app/home/components/account-select-modal/account-select-modal.component.scss diff --git a/src/app/components/account-select-modal/account-select-modal.component.ts b/src/app/home/components/account-select-modal/account-select-modal.component.ts similarity index 100% rename from src/app/components/account-select-modal/account-select-modal.component.ts rename to src/app/home/components/account-select-modal/account-select-modal.component.ts diff --git a/src/app/components/countdown-timer/countdown-timer.component.html b/src/app/home/components/countdown-timer/countdown-timer.component.html similarity index 100% rename from src/app/components/countdown-timer/countdown-timer.component.html rename to src/app/home/components/countdown-timer/countdown-timer.component.html diff --git a/src/app/components/countdown-timer/countdown-timer.component.scss b/src/app/home/components/countdown-timer/countdown-timer.component.scss similarity index 100% rename from src/app/components/countdown-timer/countdown-timer.component.scss rename to src/app/home/components/countdown-timer/countdown-timer.component.scss diff --git a/src/app/home/components/countdown-timer/countdown-timer.component.ts b/src/app/home/components/countdown-timer/countdown-timer.component.ts new file mode 100644 index 0000000..7b1f1af --- /dev/null +++ b/src/app/home/components/countdown-timer/countdown-timer.component.ts @@ -0,0 +1,56 @@ +import { Component, EventEmitter, Input, Output, OnDestroy } from '@angular/core'; +import { interval, Subscription } from 'rxjs'; +import { takeWhile, map } from 'rxjs/operators'; + +@Component({ + selector: 'app-countdown-timer', + templateUrl: './countdown-timer.component.html', + styleUrls: ['./countdown-timer.component.scss'], +}) +export class CountdownTimerComponent implements OnDestroy { + private _seconds = 0; + private timerSubscription: Subscription | null = null; + + @Input() set seconds(value: number) { + this._seconds = value; + this.timerLabel = value; + this.stopTimer(); + } + get seconds(): number { + return this._seconds; + } + + @Output() timerEnd = new EventEmitter(); + + timerLabel: number = 0; + + constructor() { } + + public startTimer() { + this.setupTimerInterval(); + } + + private stopTimer() { + if (this.timerSubscription) { + this.timerSubscription.unsubscribe(); + } + } + + private setupTimerInterval() { + this.stopTimer(); // Ensure any existing timer is stopped + + this.timerLabel = this.seconds; // reset timer label + + this.timerSubscription = interval(1000).pipe( + map(elapsed => this.seconds - elapsed - 1), + takeWhile(remaining => remaining > 0) + ).subscribe({ + next: remaining => this.timerLabel = remaining, + complete: () => this.timerEnd.emit() + }); + } + + ngOnDestroy() { + this.stopTimer(); + } +} diff --git a/src/app/home/home.module.ts b/src/app/home/home.module.ts index f460463..6a4105d 100644 --- a/src/app/home/home.module.ts +++ b/src/app/home/home.module.ts @@ -7,11 +7,12 @@ import { HomePage } from './home.page'; import { HomePageRoutingModule } from './home-routing.module'; import { AccountFilterPipe } from '../pipes/account-filter.pipe'; import { NgxScannerQrcodeModule } from 'ngx-scanner-qrcode'; -import { AccountListComponent } from '../components/account-list/account-list.component'; -import { AccountDetailComponent } from '../components/account-detail/account-detail.component'; -import { CountdownTimerComponent } from '../components/countdown-timer/countdown-timer.component'; +import { AccountListComponent } from './components/account-list/account-list.component'; +import { AccountDetailComponent } from './components/account-detail/account-detail.component'; +import { CountdownTimerComponent } from './components/countdown-timer/countdown-timer.component'; import { TranslateModule } from '@ngx-translate/core' -import { AccountSelectModalComponent } from '../components/account-select-modal/account-select-modal.component'; +import { AccountSelectModalComponent } from './components/account-select-modal/account-select-modal.component'; +import { AccountModalComponent } from './components/account-modal/account-modal.component'; @NgModule({ imports: [ @@ -24,6 +25,6 @@ import { AccountSelectModalComponent } from '../components/account-select-modal/ NgxScannerQrcodeModule, TranslateModule.forChild() ], - declarations: [HomePage, AccountListComponent, AccountDetailComponent, CountdownTimerComponent, AccountSelectModalComponent] + declarations: [HomePage, AccountListComponent, AccountDetailComponent, CountdownTimerComponent, AccountSelectModalComponent, AccountModalComponent] }) export class HomePageModule {} diff --git a/src/app/home/home.page.html b/src/app/home/home.page.html index b82407f..97feee4 100644 --- a/src/app/home/home.page.html +++ b/src/app/home/home.page.html @@ -2,10 +2,10 @@ - - + + - + @@ -79,10 +79,15 @@ - + + {{ versionInfo.versionName }} + + + {{ versionInfo.versionName }} + @@ -97,93 +102,4 @@ [accounts]="accounts$ | async | accountFilter: searchTxt" [type]="accountListType" (accountSelected)="selectAccount($event)"> - - - - - - {{ "ADD_ACCOUNT_MODAL.TITLE" | translate }} - - - - - - - - - - - - - - - - - - - - - - - - {{ "ADD_ACCOUNT_MODAL.MANUAL_INPUT" | translate }} - - - - - - - - - {{ "ADD_ACCOUNT_MODAL.SCAN_QR_CODE" | translate }} - -
- - - - - - - - - - - - - - {{ "ADD_ACCOUNT_MODAL.LOGO" | translate }} - - - - - - -

{{ "ADD_ACCOUNT_MODAL.NO_LOGO" | translate }}

-
-
- - - - - -
-
-

Logos provided by Brandfetch -

-
- {{ "ADD_ACCOUNT_MODAL.SAVE" | translate }} -
-
-
-
-
\ No newline at end of file diff --git a/src/app/home/home.page.scss b/src/app/home/home.page.scss index d3e6458..8ddff90 100644 --- a/src/app/home/home.page.scss +++ b/src/app/home/home.page.scss @@ -40,26 +40,6 @@ ion-content { height: 50%; } -.logo-card { - aspect-ratio: 1; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} - -.logo-img { - box-shadow: 0 0 3px 0 rgba(0, 0, 0, 0.2); -} - -.selected-logo { - border: 3px solid var(--ion-color-danger); -} - -.full-row { - width: 100%; -} - app-account-list.landscape { background-color: var(--ion-item-background, var(--ion-background-color, #fff)); } diff --git a/src/app/home/home.page.ts b/src/app/home/home.page.ts index 9e33ec1..0e495a3 100644 --- a/src/app/home/home.page.ts +++ b/src/app/home/home.page.ts @@ -1,19 +1,20 @@ import { Component, HostListener, OnInit, ViewChild } from '@angular/core'; import { AuthenticationService } from '../services/authentication.service'; -import { AlertController, IonModal, LoadingController, ModalController, NavController, ToastController } from '@ionic/angular'; +import { AlertController, IonPopover, LoadingController, ModalController, NavController, SearchbarCustomEvent, ToastController } from '@ionic/angular'; import { firstValueFrom, Observable, tap } from 'rxjs'; import { Account2FA } from '../models/account2FA.model'; import { Account2faService } from '../services/accounts/account2fa.service'; -import { LogoService } from '../services/logo.service'; -import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; -import { NgxScannerQrcodeComponent, ScannerQRCodeConfig, ScannerQRCodeResult } from 'ngx-scanner-qrcode'; import { LocalStorageService } from '../services/local-storage.service'; import { TranslateService } from '@ngx-translate/core'; import { GlobalUtils } from '../utils/global-utils'; -import { AccountSelectModalComponent } from '../components/account-select-modal/account-select-modal.component'; +import { AccountSelectModalComponent } from './components/account-select-modal/account-select-modal.component'; import { AppConfigService } from '../services/app-config.service'; -import { ENCRYPTION_OPTIONS_DEFAULT, EncryptionOptions, PASSWORD_CHECK_PERIOD } from '../models/encryption-options.model'; +import { ENCRYPTION_OPTIONS_DEFAULT, EncryptionOptions } from '../models/encryption-options.model'; import { MigrationService } from '../services/migration.service'; +import { LoggingService } from '../services/logging.service'; +import { AppVersionInfo } from '../models/app-version.enum'; +import { PasswordService } from './password.service'; +import { AccountModalComponent } from './components/account-modal/account-modal.component'; @Component({ selector: 'app-home', @@ -21,92 +22,44 @@ import { MigrationService } from '../services/migration.service'; styleUrls: ['home.page.scss'], }) export class HomePage implements OnInit { - @ViewChild('popover') popover: any; - @ViewChild(IonModal) modal!: IonModal; - @ViewChild('qrscanner') qrscanner!: NgxScannerQrcodeComponent; + @ViewChild('popover') popover!: IonPopover; @HostListener('window:resize', ['$event']) onWindowResize() { this.isLandscape = window.innerWidth > window.innerHeight } - @HostListener('window:focus', ['$event']) - onFocus(event: FocusEvent): void { - // TODO: resume timer - this.isWindowFocused = true - } - - @HostListener('window:blur', ['$event']) - onBlur(event: FocusEvent): void { - // TODO: stop timer, camera, etc - this.isWindowFocused = false - } - - qrScannerOpts: ScannerQRCodeConfig = { - isBeep: false, - vibrate: 100, - constraints: { - video: { - facingMode: 'environment' - } - } - } - accounts$: Observable = new Observable(); selectedAccount?: Account2FA searchTxt: string = '' - draftLogoSearchTxt: string = '' - searchLogoResults: any[] = [] - draftLogoURL: string = '' - validations_form: FormGroup; - manualInput: boolean = false - isPopoverOpen: boolean = false - isAddAccountModalOpen: boolean = false - isScanActive: boolean = false isWindowFocused: boolean = true hasLockedAccounts: boolean = true - versionInfo: any + versionInfo: AppVersionInfo + versionClickCount = 0 private encryptionOptions: EncryptionOptions = ENCRYPTION_OPTIONS_DEFAULT private systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)'); private isLandscape: boolean = false private currentDarkModePref: string = ''; private shouldAlertAboutLockedAccounts: boolean = true + private loading: HTMLIonLoadingElement | undefined = undefined + constructor( private authService: AuthenticationService, private accountsService: Account2faService, - private loadingController: LoadingController, + private loadingCtrl: LoadingController, private toastController: ToastController, private alertController: AlertController, private modalController: ModalController, - private logoService: LogoService, private storageService: LocalStorageService, private configService: AppConfigService, private migrationService: MigrationService, private translateService: TranslateService, + private loggingService: LoggingService, + private passwordService: PasswordService, private navCtrl: NavController, - formBuilder: FormBuilder ) { - this.validations_form = formBuilder.group({ - label: new FormControl('', Validators.compose([ - Validators.required, - ])), - secret: new FormControl('', Validators.compose([ - Validators.required, - Validators.minLength(8), - Validators.pattern('^[A-Z2-7]+=*$') - ])), - tokenLength: new FormControl(6, Validators.compose([ - Validators.required, - Validators.pattern('^[1-9]+[0-9]*$') - ])), - interval: new FormControl(30, Validators.compose([ - Validators.required, - Validators.pattern('^[1-9]+[0-9]*$') - ])), - }); - this.versionInfo = this.configService.versionInfo } @@ -158,58 +111,44 @@ export class HomePage implements OnInit { } const message = await firstValueFrom(this.translateService.get('HOME.LOGGING_OUT')) - const loading = await this.loadingController.create({ - message, - spinner: "circular", - backdropDismiss: false - }) - await loading.present() + await this.presentLoading(message) await this.accountsService.clearCache() await this.authService.logout() await this.storageService.clearStorage() //reload window - await loading.dismiss() + await this.dismissLoading() await this.navCtrl.navigateRoot('/').then(() => { window.location.reload() }) } - selectAccount(account: any) { + selectAccount(account: Account2FA) { this.selectedAccount = account if (account && account.id) { this.storageService.set('lastSelectedAccountId', account.id) } } - handleSearch(evt: any) { + handleSearch(evt: SearchbarCustomEvent) { const searchTerm = evt?.detail?.value console.log({ evt, searchTerm }) - this.searchTxt = searchTerm + this.searchTxt = searchTerm ?? '' } - async handleSearchLogo(evt: any) { - const searchTerm = evt?.detail?.value - console.log({ evt, searchTerm }) - if (!searchTerm) { - this.draftLogoURL = '' - this.searchLogoResults = [] - return - } - const brandInfo = await this.logoService.searchServiceInfo(searchTerm) - if (brandInfo && brandInfo.length > 0) { - this.draftLogoURL = brandInfo[0].logo - this.searchLogoResults = brandInfo.map(brand => brand.logo) - } - console.log({ brandInfo, draftLogoURL: this.draftLogoURL, searchTerm: this.draftLogoSearchTxt, results: this.searchLogoResults }) - } + async addAccountAction() { + // show modal + const modal = await this.modalController.create({ + component: AccountModalComponent, + backdropDismiss: false + }) - selectLogo(logoURL: string) { - this.draftLogoURL = logoURL - } + await modal.present() + const { data, role } = await modal.onWillDismiss(); - async addAccountAction() { - this.isAddAccountModalOpen = true - this.scanCode() + if (role === 'added' && data) { + const newAccount = data as Account2FA + await this.createAccount(newAccount) + } } async exportAccountAction() { @@ -261,17 +200,13 @@ export class HomePage implements OnInit { if (selectedAccounts && selectedAccounts.length > 0) { console.log("Selected accounts to export", { selectedAccounts }) const message = await firstValueFrom(this.translateService.get('ACCOUNT_SYNC.EXPORTING_ACCOUNTS')) - const loading = await this.loadingController.create({ - message, - backdropDismiss: false - }) - await loading.present() + await this.presentLoading(message) await this.accountsService.exportAccounts(selectedAccounts, exportWithEncryption) - await loading.dismiss() + await this.dismissLoading() } else { console.log("No accounts selected to export") const message = await firstValueFrom(this.translateService.get('ACCOUNT_SYNC.ERROR.NO_ACCOUNTS_SELECTED_TO_EXPORT')) - await this.showError(message) + await this.showAlert(message) } } @@ -284,12 +219,8 @@ export class HomePage implements OnInit { const file = (e.target as HTMLInputElement).files?.[0] if (file) { let message = await firstValueFrom(this.translateService.get('HOME.LOADING_ACCOUNTS_FILE')) - let loading = await this.loadingController.create({ - message, - backdropDismiss: false - }) + await this.presentLoading(message) try { - await loading.present() const accounts = await this.accountsService.readAccountsFromFile(file) input.remove() const title = await firstValueFrom(this.translateService.get('ACCOUNT_SYNC.IMPORT_ACCOUNTS_MODAL_TITLE')) @@ -302,7 +233,7 @@ export class HomePage implements OnInit { confirmText }, }) - await loading.dismiss() + await this.dismissLoading() modal.present() const { data, role } = await modal.onWillDismiss(); if (role === 'cancel') { @@ -349,9 +280,10 @@ export class HomePage implements OnInit { const message = await firstValueFrom(this.translateService.get('ACCOUNT_SYNC.ERROR.INVALID_PASSWORD')) throw new Error(message) } + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { const message = error && error.message || await firstValueFrom(this.translateService.get('ACCOUNT_SYNC.ERROR.GENERIC_IMPORT_ERROR')) - await this.showError(message) + await this.showAlert(message) return } } @@ -359,22 +291,19 @@ export class HomePage implements OnInit { // import the accounts message = await firstValueFrom(this.translateService.get('ACCOUNT_SYNC.IMPORTING_ACCOUNTS')) - loading = await this.loadingController.create({ - message, - backdropDismiss: false - }) - await loading.present() + await this.presentLoading(message) console.log({data, selectedAccounts}) await this.accountsService.importAccounts(selectedAccounts) - await loading.dismiss() + await this.dismissLoading() } else { throw new Error("ACCOUNT_SYNC.ERROR.NO_ACCOUNTS_SELECTED_TO_IMPORT") } + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { - let errorKey = error && error.message || 'ACCOUNT_SYNC.ERROR.GENERIC_IMPORT_ERROR' + const errorKey = error && error.message || 'ACCOUNT_SYNC.ERROR.GENERIC_IMPORT_ERROR' const message = await firstValueFrom(this.translateService.get(errorKey)) const header = await firstValueFrom(this.translateService.get('ACCOUNT_SYNC.ERROR.IMPORT_ERROR_TITLE')) - await this.showError(message, header) + await this.showAlert(message, header) } } } @@ -386,7 +315,7 @@ export class HomePage implements OnInit { } else { await this.activateEncryption() } - this.hidePopover() + this.popover?.dismiss() } async unlockAccountsAction() { @@ -411,66 +340,16 @@ export class HomePage implements OnInit { await this.configService.setEncryptionOptions(this.encryptionOptions) } - showPopover(e: Event) { - this.popover.event = null - this.popover.event = e; - this.isPopoverOpen = false; - setTimeout(() => { - this.isPopoverOpen = true; - }, 50); - } - - private hidePopover() { - this.isPopoverOpen = false; - } - - onDidDismissModal(e: Event) { - this.isAddAccountModalOpen = false - this.manualInput = false - this.isAddAccountModalOpen = false - this.isScanActive = false - if (this.qrscanner) { - this.qrscanner.stop() - } - // clear form - this.validations_form.reset() - this.draftLogoURL = '' - this.draftLogoSearchTxt = '' - this.searchLogoResults = [] - } - - onWillDismissModal(e: Event) { - console.log("Will dismiss modal", e) - if (this.qrscanner) { - console.log("STOP QR") - this.qrscanner.stop() - } - } - - async closeAddAccountModal() { - await this.modal.dismiss() - } - - async createAccount(formValues: any) { - console.log({ formValues }) - const logo = this.draftLogoURL - await this.closeAddAccountModal() - const newAccountDict = Object.assign(formValues, { logo, active: true }) - const account = Account2FA.fromDictionary(newAccountDict) - console.log({ account2fa: account }) + async createAccount(newAccount: Account2FA) { const message = await firstValueFrom(this.translateService.get('ADD_ACCOUNT_MODAL.ADDING_ACCOUNT')) - const loading = await this.loadingController.create({ - message, - backdropDismiss: false - }) - await loading.present() + await this.presentLoading(message) try { - await this.accountsService.addAccount(account) - await loading.dismiss() + await this.accountsService.addAccount(newAccount) + await this.dismissLoading() // select new account - this.selectAccount(account) - } catch (error: any) { - await loading.dismiss() + this.selectAccount(newAccount) + } catch (error: any) { // eslint-disable-line @typescript-eslint/no-explicit-any + await this.dismissLoading() const messageKey = error.message === 'INVALID_SESSION' ? await firstValueFrom(this.translateService.get('ADD_ACCOUNT_MODAL.ERROR_MSGS.INVALID_SESSION')) : await firstValueFrom(this.translateService.get('ADD_ACCOUNT_MODAL.ERROR_MSGS.ERROR_ADDING_ACCOUNT')) @@ -523,107 +402,8 @@ export class HomePage implements OnInit { document.documentElement.classList.toggle('ion-palette-dark', isDark); } - async scanCode() { - // - this.isScanActive = true - const message = await firstValueFrom(this.translateService.get('ADD_ACCOUNT_MODAL.LOADING_CAMERA')) - const loading = await this.loadingController.create({ - message, - backdropDismiss: false - }) - await loading.present() - try { - await firstValueFrom(this.qrscanner.start()) - } catch (error) { - // camera permission denial - const header = await firstValueFrom(this.translateService.get('ADD_ACCOUNT_MODAL.ERROR_MSGS.ERROR_CAMERA_HEADER')) - const message = await firstValueFrom(this.translateService.get('ADD_ACCOUNT_MODAL.ERROR_MSGS.ERROR_CAMERA_MESSAGE')) - await loading.dismiss() - await this.showError(message, header) - this.manualInput = true - this.isScanActive = false - return - } - - const devices = (await firstValueFrom(this.qrscanner.devices)).filter(device => device.kind === 'videoinput') - console.log({ devices }) - // find back camera - let backCamera = devices.find(device => device.label.toLowerCase().includes('back camera')) - if (!backCamera) { - backCamera = devices.find(device => device.label.toLowerCase().match(/.*back.*camera.*/)) - } - - if (backCamera && backCamera.deviceId) { - console.log("using device", { backCamera }) - await this.qrscanner.playDevice(backCamera.deviceId) - } - await loading.dismiss() - } - - async onQRCodeScanned(evt: ScannerQRCodeResult[], qrscanner: NgxScannerQrcodeComponent) { - try { - await qrscanner.stop() - await this.qrscanner.stop() - } catch (error) { - console.error("Error stopping scanner", error) - } - this.processQRCode(evt && evt[0]?.value || '') - // give time for the scanner to stop - setTimeout(() => { - this.isScanActive = false - }, 200); - } - - async cycleCamera() { - console.log("cycle camera") - const current = this.qrscanner.deviceIndexActive - console.log({ current }) - const devices = await firstValueFrom(this.qrscanner.devices) - console.log({ devices }) - const next = (current + 1) % devices.length - const nextDevice = devices[next] - this.qrscanner.playDevice(nextDevice.deviceId) - } - - manualInputAction() { - if (this.qrscanner) { - console.log("STOP QR Reading") - this.qrscanner.stop() - } - this.isScanActive = false; - this.manualInput = true - } - - private async processQRCode(evt: string) { - // The URI format and params is described in https://github.com/google/google-authenticator/wiki/Key-Uri-Format - // otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30 - try { - const account = Account2FA.fromOTPAuthURL(evt) - console.log({ account }) - - this.validations_form.controls['label'].setValue(account.label) - this.validations_form.controls['secret'].setValue(account.secret) - this.validations_form.controls['tokenLength'].setValue(account.tokenLength) - this.validations_form.controls['interval'].setValue(account.interval) - // service name inferred from issuer or label - const serviceName = account.issuer || account.label.split(':')[0] - const event = { detail: { value: serviceName } } - this.handleSearchLogo(event) - } catch (error) { - const message = await firstValueFrom(this.translateService.get('ADD_ACCOUNT_MODAL.ERROR_MSGS.INVALID_QR_CODE')) - console.error("Error processing QR code", error) - const toast = await this.toastController.create({ - message, - duration: 2000, - color: 'danger' - }) - await toast.present() - } - this.manualInput = true - } - private confirmLogout(): Promise { - return new Promise(async (resolve, reject) => { + return new Promise(async (resolve) => { const title = await firstValueFrom(this.translateService.get('HOME.CONFIRM_LOGOUT_TITLE')) const message = await firstValueFrom(this.translateService.get('HOME.CONFIRM_LOGOUT_MESSAGE')) const yesBtnText = await firstValueFrom(this.translateService.get('HOME.CONFIRM_LOGOUT_YES')) @@ -653,12 +433,7 @@ export class HomePage implements OnInit { private async loadAccounts() { const loadingMsg = await firstValueFrom(this.translateService.get('HOME.LOADING_ACCOUNTS')) - const loading = await this.loadingController.create({ - message: loadingMsg, - backdropDismiss: false - }) - await loading.present() - + await this.presentLoading(loadingMsg) const accounts$ = (await this.accountsService.getAccounts()).pipe(tap(accounts => { const lockedAccounts = accounts.filter(account => account.isLocked) this.hasLockedAccounts = lockedAccounts.length > 0 @@ -670,7 +445,7 @@ export class HomePage implements OnInit { console.log("Accounts tapped", { accounts }) })) - await loading.dismiss() + await this.dismissLoading() this.accounts$ = accounts$ } @@ -699,12 +474,12 @@ export class HomePage implements OnInit { if(this.isEncryptionActive) { // step 1 const password = await this.configService.getEncryptionKey() if(!password) { // 1.1 - const success = await this.setupNewPassword() // 1.1 + const success = await this.passwordService.setupNewPassword() // 1.1 if(!success) { // 1.1.1 // Deactivate encryption await this.deactivateEncryption() // Show failed password setup message - await this.showFailedPasswordSetupAlert() + await this.passwordService.showFailedPasswordSetupAlert() return // exit encryption setup flow } } @@ -712,13 +487,13 @@ export class HomePage implements OnInit { // 1.2 if (this.shouldPeriodicCheckPassword) { // 1.2.1 console.log("Starting periodic password check") - await this.periodicPasswordCheck() + await this.passwordService.periodicPasswordCheck() } } } private async activateEncryption(): Promise { - const success = await this.setupNewPassword() + const success = await this.passwordService.setupNewPassword() if(!success) { this.setEncryptionActive(false) return @@ -749,167 +524,7 @@ export class HomePage implements OnInit { await this.saveEncryptionOptions() } - private async showFailedPasswordSetupAlert() { - const title = await firstValueFrom(this.translateService.get('HOME.ERRORS.PASSWORD_NOT_SET_TITLE')) - const message = await firstValueFrom(this.translateService.get('HOME.ERRORS.PASSWORD_NOT_SET')) - this.showError(message, title) - } - - private async setupNewPassword(): Promise { - const passwordData = await this.promptPassword() - if(passwordData && passwordData.password && passwordData.password === passwordData.passwordConfirmation) { - await this.configService.setEncryptionKey(passwordData.password) - await this.configService.setLastPasswordCheck() - return true // 1.1.2 - } - return false // 1.1.1 - } - - private async promptPassword(): Promise<{password: string, passwordConfirmation: string}> { - const title = await firstValueFrom(this.translateService.get('HOME.PASSWORD_PROMPT_TITLE')) - const message = await firstValueFrom(this.translateService.get('HOME.PASSWORD_PROMPT_MESSAGE')) - const passwordPlaceholder = await firstValueFrom(this.translateService.get('HOME.PASSWORD_PROMPT_PLACEHOLDER')) - const passwordConfirmationPlaceholder = await firstValueFrom(this.translateService.get('HOME.PASSWORD_PROMPT_CONFIRMATION_PLACEHOLDER')) - const cancelText = await firstValueFrom(this.translateService.get('HOME.PASSWORD_PROMPT_CANCEL')) - const okText = await firstValueFrom(this.translateService.get('HOME.PASSWORD_PROMPT_CONFIRM')) - - const alert = await this.alertController.create({ - header: title, - message, - backdropDismiss: false, - inputs: [ - { - name: 'password', - type: 'password', - placeholder: passwordPlaceholder - }, - { - name: 'passwordConfirmation', - type: 'password', - placeholder: passwordConfirmationPlaceholder - } - ], - buttons: [ - { - text: cancelText, - role: 'cancel' - }, { - text: okText, - handler: (data) => { - return { password: data.password, passwordConfirmation: data.passwordConfirmation } - } - } - ] - }) - await alert.present() - - const { data } = await alert.onDidDismiss() - if(!data) { - return { password: '', passwordConfirmation: '' } - } - return data.values - } - - private async periodicPasswordCheck() { - const lastCheck = await this.configService.getLastPasswordCheck() - const nextCheck = lastCheck + PASSWORD_CHECK_PERIOD - const now = Date.now() - console.log({ lastCheck, nextCheck, now }) - if(now >= nextCheck) { // 1.2.1 - const checkSuccess = await this.presentPasswordCheckAlert() - if(checkSuccess) { // 1.2.1.1 - console.log('password check success') - await this.configService.setLastPasswordCheck() - } else { // 1.2.1.2 - await this.alertUserAboutInabilityToRecoverPassword() - } - } - } - - private async alertUserAboutInabilityToRecoverPassword() { - const title = await firstValueFrom(this.translateService.get('HOME.PASSWORD_RECOVERY_ALERT.TITLE')) - const message = await firstValueFrom(this.translateService.get('HOME.PASSWORD_RECOVERY_ALERT.MESSAGE')) - await this.showError(message, title) - } - - private async presentPasswordCheckAlert(): Promise { - const title = await firstValueFrom(this.translateService.get('HOME.PERIODIC_CHECK.TITLE')) - const message = await firstValueFrom(this.translateService.get('HOME.PERIODIC_CHECK.MESSAGE')) - const confirm = await firstValueFrom(this.translateService.get('HOME.PERIODIC_CHECK.CONFIRM')) - const cancelLabel = await firstValueFrom(this.translateService.get('HOME.CANCEL')) - const passwordPlaceholder = await firstValueFrom(this.translateService.get('HOME.PASSWORD_PROMPT_CONFIRMATION_PLACEHOLDER')) - const alert = await this.alertController.create({ - header: title, - message, - backdropDismiss: false, - inputs: [ - { - type: 'password', - name: 'password', - placeholder: passwordPlaceholder - } - ], - buttons: [ - { - text: cancelLabel, - role: 'cancel' - }, - { - text: confirm, - role: 'confirm', - handler: (data) => { - return data.password - } - } - ] - }) - await alert.present() - - const { data, role } = await alert.onDidDismiss() - if (role === 'confirm') { - console.log('password check', { data }) - const password = data.values.password - if(password !== await this.configService.getEncryptionKey()) { - const tryAgain = await this.presentPasswordCheckMismatchAlert() - if(tryAgain) { - return await this.presentPasswordCheckAlert() - } - } else { - return true - } - } - return false - } - - private async presentPasswordCheckMismatchAlert(): Promise { - const title = await firstValueFrom(this.translateService.get('HOME.PERIODIC_CHECK.MISMATCH.TITLE')) - const message = await firstValueFrom(this.translateService.get('HOME.PERIODIC_CHECK.MISMATCH.MESSAGE')) - const tryAgainLabel = await firstValueFrom(this.translateService.get('HOME.PERIODIC_CHECK.MISMATCH.TRY_AGAIN')) - const cancelLabel = await firstValueFrom(this.translateService.get('HOME.CANCEL')) - const alert = await this.alertController.create({ - header: title, - message, - backdropDismiss: false, - buttons: [ - { - text: cancelLabel, - role: 'cancel' - }, - { - text: tryAgainLabel, - role: 'try_again' - } - ] - }) - await alert.present() - const { role } = await alert.onDidDismiss() - if(role === 'try_again') { - return true - } - return false - } - - private async promptUnlockPassword(lockedAccounts: Account2FA[]): Promise { + private async promptUnlockPassword(lockedAccounts: Account2FA[]): Promise { // TODO: refactor this, and move to password service if(!lockedAccounts.some(account => account.isLocked)) { const message = await firstValueFrom(this.translateService.get('HOME.ASK_PASSWORD.ERROR_NOT_LOCKED')) throw new Error(message) @@ -919,7 +534,7 @@ export class HomePage implements OnInit { let password = '' do { - password = await this.promptForPassword() + password = await this.passwordService.promptUnlockPassword() if (!password) { break } else { @@ -930,7 +545,7 @@ export class HomePage implements OnInit { success = true } catch (error) { const message = await firstValueFrom(this.translateService.get('HOME.ERRORS.INVALID_PASSWORD')) - await this.showError(message) + await this.showAlert(message) } } } while (!success); @@ -938,43 +553,6 @@ export class HomePage implements OnInit { return password } - private async promptForPassword(): Promise { - const message = await firstValueFrom(this.translateService.get('HOME.ASK_PASSWORD.MESSAGE')) - const confirm = await firstValueFrom(this.translateService.get('HOME.ASK_PASSWORD.CONFIRM')) - const cancelLabel = await firstValueFrom(this.translateService.get('HOME.CANCEL')) - const passwordPlaceholder = await firstValueFrom(this.translateService.get('HOME.PASSWORD_PROMPT_CONFIRMATION_PLACEHOLDER')) - const alert = await this.alertController.create({ - message, - backdropDismiss: false, - inputs: [ - { - type: 'password', - name: 'password', - placeholder: passwordPlaceholder - } - ], - buttons: [ - { - text: cancelLabel, - role: 'cancel' - }, - { - text: confirm, - role: 'confirm', - handler: (data) => { - return data.password - } - } - ] - }) - await alert.present() - const { data } = await alert.onDidDismiss() - if(data && data.values) { - return data.values.password - } - return '' - } - private async handleAccountsLocked(lockedAccounts: Account2FA[]): Promise { if(await this.alertAccountsLocked()) { // user wants to informPassword const password = await this.promptUnlockPassword(lockedAccounts) @@ -1025,7 +603,14 @@ export class HomePage implements OnInit { return willInputPassword } - async showVersionInfo(): Promise { + async showVersionInfoAction(): Promise { + this.versionClickCount += 1 + if(this.versionClickCount < 3) { + return + } + if(this.versionClickCount === 3) { + this.loggingService.enableConsole() + } const versionInfo = this.configService.versionInfo const title = await firstValueFrom(this.translateService.get('HOME.VERSION_INFO.VERSION_INFO_TITLE')) const versionLabel = await firstValueFrom(this.translateService.get('HOME.VERSION_INFO.VERSION_LABEL')) @@ -1036,10 +621,14 @@ export class HomePage implements OnInit {

${buildDateLabel}: ${versionInfo.buildDate}

${gitHashLabel}: ${versionInfo.commitHash}

` - await this.showError(message, title) + await this.showAlert(message, title) } - private async showError(message: string, header?: string): Promise { + async enableLogging() { + await this.loggingService.enableConsole() + } + + private async showAlert(message: string, header?: string): Promise { const title = header const okLabel = await firstValueFrom(this.translateService.get('HOME.OK')) const alert = await this.alertController.create({ @@ -1050,4 +639,24 @@ export class HomePage implements OnInit { await alert.present() await alert.onDidDismiss() } + + private async presentLoading(message: string): Promise { + // if loading is already present, update message + if(this.loading != undefined) { + this.loading.message = message + return + } + + // create new loading + this.loading = await this.loadingCtrl.create({ + message, + backdropDismiss: false + }) + await this.loading.present() + } + + private async dismissLoading(): Promise { + await this.loading?.dismiss() + this.loading = undefined + } } \ No newline at end of file diff --git a/src/app/home/password.service.ts b/src/app/home/password.service.ts new file mode 100644 index 0000000..df4eacb --- /dev/null +++ b/src/app/home/password.service.ts @@ -0,0 +1,230 @@ +import { Injectable } from '@angular/core'; +import { AppConfigService } from '../services/app-config.service'; +import { firstValueFrom } from 'rxjs'; +import { TranslateService } from '@ngx-translate/core'; +import { AlertController } from '@ionic/angular'; +import { PASSWORD_CHECK_PERIOD } from '../models/encryption-options.model'; + +@Injectable({ + providedIn: 'root' +}) + +/** + * Service to handle password related operations + */ +export class PasswordService { + constructor( + private alertController: AlertController, + private configService: AppConfigService, + private translateService: TranslateService + ) { } + + public async setupNewPassword(): Promise { + const passwordData = await this.promptNewPassword() + if(passwordData && passwordData.password && passwordData.password === passwordData.passwordConfirmation) { + await this.configService.setEncryptionKey(passwordData.password) + await this.configService.setLastPasswordCheck() + return true + } + return false + } + + public async periodicPasswordCheck() { + const lastCheck = await this.configService.getLastPasswordCheck() + const nextCheck = lastCheck + PASSWORD_CHECK_PERIOD + const now = Date.now() + console.log({ lastCheck, nextCheck, now }) + if(now >= nextCheck) { // 1.2.1 + const checkSuccess = await this.presentPasswordCheckAlert() + if(checkSuccess) { // 1.2.1.1 + console.log('password check success') + await this.configService.setLastPasswordCheck() + } else { // 1.2.1.2 + await this.alertUserAboutInabilityToRecoverPassword() + } + } + } + + public async showFailedPasswordSetupAlert() { + const title = await firstValueFrom(this.translateService.get('HOME.ERRORS.PASSWORD_NOT_SET_TITLE')) + const message = await firstValueFrom(this.translateService.get('HOME.ERRORS.PASSWORD_NOT_SET')) + this.showAlert(message, title) + } + + private async presentPasswordCheckAlert(): Promise { + const title = await firstValueFrom(this.translateService.get('HOME.PERIODIC_CHECK.TITLE')) + const message = await firstValueFrom(this.translateService.get('HOME.PERIODIC_CHECK.MESSAGE')) + const confirm = await firstValueFrom(this.translateService.get('HOME.PERIODIC_CHECK.CONFIRM')) + const cancelLabel = await firstValueFrom(this.translateService.get('HOME.CANCEL')) + const passwordPlaceholder = await firstValueFrom(this.translateService.get('HOME.PASSWORD_PROMPT_CONFIRMATION_PLACEHOLDER')) + const alert = await this.alertController.create({ + header: title, + message, + backdropDismiss: false, + inputs: [ + { + type: 'password', + name: 'password', + placeholder: passwordPlaceholder + } + ], + buttons: [ + { + text: cancelLabel, + role: 'cancel' + }, + { + text: confirm, + role: 'confirm', + handler: (data) => { + return data.password + } + } + ] + }) + await alert.present() + + const { data, role } = await alert.onDidDismiss() + if (role === 'confirm') { + console.log('password check', { data }) + const password = data.values.password + if(password !== await this.configService.getEncryptionKey()) { + const tryAgain = await this.presentPasswordCheckMismatchAlert() + if(tryAgain) { + return await this.presentPasswordCheckAlert() + } + } else { + return true + } + } + return false + } + + private async promptNewPassword(): Promise<{password: string, passwordConfirmation: string}> { + const title = await firstValueFrom(this.translateService.get('HOME.PASSWORD_PROMPT_TITLE')) + const message = await firstValueFrom(this.translateService.get('HOME.PASSWORD_PROMPT_MESSAGE')) + const passwordPlaceholder = await firstValueFrom(this.translateService.get('HOME.PASSWORD_PROMPT_PLACEHOLDER')) + const passwordConfirmationPlaceholder = await firstValueFrom(this.translateService.get('HOME.PASSWORD_PROMPT_CONFIRMATION_PLACEHOLDER')) + const cancelText = await firstValueFrom(this.translateService.get('HOME.PASSWORD_PROMPT_CANCEL')) + const okText = await firstValueFrom(this.translateService.get('HOME.PASSWORD_PROMPT_CONFIRM')) + + const alert = await this.alertController.create({ + header: title, + message, + backdropDismiss: false, + inputs: [ + { + name: 'password', + type: 'password', + placeholder: passwordPlaceholder + }, + { + name: 'passwordConfirmation', + type: 'password', + placeholder: passwordConfirmationPlaceholder + } + ], + buttons: [ + { + text: cancelText, + role: 'cancel' + }, { + text: okText, + handler: (data) => { + return { password: data.password, passwordConfirmation: data.passwordConfirmation } + } + } + ] + }) + await alert.present() + + const { data } = await alert.onDidDismiss() + if(!data) { + return { password: '', passwordConfirmation: '' } + } + return data.values + } + + public async promptUnlockPassword(): Promise { + const message = await firstValueFrom(this.translateService.get('HOME.ASK_PASSWORD.MESSAGE')) + const confirm = await firstValueFrom(this.translateService.get('HOME.ASK_PASSWORD.CONFIRM')) + const cancelLabel = await firstValueFrom(this.translateService.get('HOME.CANCEL')) + const passwordPlaceholder = await firstValueFrom(this.translateService.get('HOME.PASSWORD_PROMPT_CONFIRMATION_PLACEHOLDER')) + const alert = await this.alertController.create({ + message, + backdropDismiss: false, + inputs: [ + { + type: 'password', + name: 'password', + placeholder: passwordPlaceholder + } + ], + buttons: [ + { + text: cancelLabel, + role: 'cancel' + }, + { + text: confirm, + role: 'confirm', + handler: (data) => { + return data.password + } + } + ] + }) + await alert.present() + const { data } = await alert.onDidDismiss() + if(data && data.values) { + return data.values.password + } + return '' + } + + private async alertUserAboutInabilityToRecoverPassword() { + const title = await firstValueFrom(this.translateService.get('HOME.PASSWORD_RECOVERY_ALERT.TITLE')) + const message = await firstValueFrom(this.translateService.get('HOME.PASSWORD_RECOVERY_ALERT.MESSAGE')) + await this.showAlert(message, title) + } + + private async presentPasswordCheckMismatchAlert(): Promise { + const title = await firstValueFrom(this.translateService.get('HOME.PERIODIC_CHECK.MISMATCH.TITLE')) + const message = await firstValueFrom(this.translateService.get('HOME.PERIODIC_CHECK.MISMATCH.MESSAGE')) + const tryAgainLabel = await firstValueFrom(this.translateService.get('HOME.PERIODIC_CHECK.MISMATCH.TRY_AGAIN')) + const cancelLabel = await firstValueFrom(this.translateService.get('HOME.CANCEL')) + const alert = await this.alertController.create({ + header: title, + message, + backdropDismiss: false, + buttons: [ + { + text: cancelLabel, + role: 'cancel' + }, + { + text: tryAgainLabel, + role: 'try_again' + } + ] + }) + await alert.present() + const { role } = await alert.onDidDismiss() + if(role === 'try_again') { + return true + } + return false + } + + private async showAlert(message: string, header?: string): Promise { + const title = header + const okLabel = await firstValueFrom(this.translateService.get('HOME.OK')) + const alert = await this.alertController.create({ + header: title, + message, + buttons: [okLabel] + }) + await alert.present() + await alert.onDidDismiss() + } +} diff --git a/src/app/login/login.page.scss b/src/app/login/login.page.scss index 4c2f956..e355263 100644 --- a/src/app/login/login.page.scss +++ b/src/app/login/login.page.scss @@ -28,7 +28,7 @@ width: 30%; } -.login-bg, #background-content { +.login-bg{ height: 100%; background: linear-gradient(to right, var(--ion-color-light) 0%, var(--ion-color-light) 50%, var(--ion-color-dark) 50%, var(--ion-color-dark) 100%) !important; } diff --git a/src/app/login/login.page.ts b/src/app/login/login.page.ts index 5763f32..1b1b510 100644 --- a/src/app/login/login.page.ts +++ b/src/app/login/login.page.ts @@ -81,7 +81,7 @@ export class LoginPage implements OnInit { await this.appConfig.setOfflineMode(false) await loading.dismiss() this.navCtrl.navigateForward('/home', {skipLocationChange: true}) - } catch (error: any) { + } catch (error: any) { // eslint-disable-line @typescript-eslint/no-explicit-any switch(error.code) { case 'auth/user-not-found': errorMessage = "LOGIN.ERROR_MSGS.USER_NOT_FOUND" diff --git a/src/app/models/account2FA.model.ts b/src/app/models/account2FA.model.ts index 5cfdd9d..8abadb4 100644 --- a/src/app/models/account2FA.model.ts +++ b/src/app/models/account2FA.model.ts @@ -11,7 +11,7 @@ export interface IAccount2FA { issuer?: string; active?: boolean; logo?: string; - added?: any; + added?: any; // eslint-disable-line @typescript-eslint/no-explicit-any encryptedSecret?: string; iv?: string; salt?: string; @@ -35,11 +35,11 @@ export class Account2FA implements IAccount2FA { issuer?: string; active?: boolean; logo?: string; - added?: any; + added?: any; // eslint-disable-line @typescript-eslint/no-explicit-any encryptedSecret?: string; iv?: string; salt?: string; - constructor(id: string, label: string, secret?: string, tokenLength?: number, interval?: number, algorithm?: string, issuer?:string, active?: boolean, logo?: string, encryptedSecret?: string, iv?: string, salt?: string, added?: any) { + constructor(id: string, label: string, secret?: string, tokenLength?: number, interval?: number, algorithm?: string, issuer?:string, active?: boolean, logo?: string, encryptedSecret?: string, iv?: string, salt?: string, added?: any) { // eslint-disable-line @typescript-eslint/no-explicit-any this.id = id; this.label = label; this.secret = secret || ''; @@ -55,7 +55,7 @@ export class Account2FA implements IAccount2FA { this.added = added; } - static fromDictionary(dict: any): Account2FA { + static fromDictionary(dict: any): Account2FA { // eslint-disable-line @typescript-eslint/no-explicit-any if(!dict || typeof dict !== 'object') { throw new Error('Invalid dictionary'); } @@ -166,7 +166,7 @@ export class Account2FA implements IAccount2FA { return this.interval - (Math.floor(Date.now() / 1000) % this.interval); } - typeErased(): Object { + typeErased(): Object { // eslint-disable-line @typescript-eslint/ban-types const typeErasedObject = Object.assign({}, this); for (const key in typeErasedObject) { if (!typeErasedObject[key]) { diff --git a/src/app/models/app-version.enum.ts b/src/app/models/app-version.enum.ts index 6483a92..9241046 100644 --- a/src/app/models/app-version.enum.ts +++ b/src/app/models/app-version.enum.ts @@ -9,3 +9,10 @@ export enum AppVersion { V2_0_0 = '2.0.0', V2_1_0 = '2.1.0' } + +export interface AppVersionInfo { + versionNumber: string; + buildDate: string; + commitHash: string; + versionName: string; +} diff --git a/src/app/models/brand-fetch-search.model.ts b/src/app/models/brand-fetch-search.model.ts index d0d3aca..e5c7b49 100644 --- a/src/app/models/brand-fetch-search.model.ts +++ b/src/app/models/brand-fetch-search.model.ts @@ -1,9 +1,12 @@ -export interface BrandFetchSearchResult { +export interface BrandFetchSearchAPIResult { brandId: string; claimed: boolean; domain: string; icon: string; name: string; +} + +export interface BrandFetchSearchResult extends BrandFetchSearchAPIResult { logo: string; } diff --git a/src/app/services/accounts/remote-account2fa.service.ts b/src/app/services/accounts/remote-account2fa.service.ts index 7046f0a..e0be9ca 100644 --- a/src/app/services/accounts/remote-account2fa.service.ts +++ b/src/app/services/accounts/remote-account2fa.service.ts @@ -1,8 +1,8 @@ import { inject, Injectable } from '@angular/core'; import { Account2FA, IAccount2FA, IAccount2FAProvider } from '../../models/account2FA.model'; -import { BehaviorSubject, map, Observable, throttleTime } from 'rxjs'; +import { BehaviorSubject, map, Observable } from 'rxjs'; import { AuthenticationService } from '../authentication.service'; -import { clearIndexedDbPersistence, collection, collectionData, doc, Firestore, orderBy, query, runTransaction, serverTimestamp, setDoc, terminate, Timestamp, where, writeBatch } from '@angular/fire/firestore'; +import { clearIndexedDbPersistence, collection, collectionData, doc, Firestore, orderBy, query, runTransaction, serverTimestamp, setDoc, terminate, where } from '@angular/fire/firestore'; import { LocalAccount2faService } from './local-account2fa.service'; @Injectable({ @@ -81,7 +81,7 @@ export class RemoteAccount2faService implements IAccount2FAProvider { this.accountsSubject.next(accounts) }) // start a timeout to give a chance for the remote service to load - const timeoutPromise = new Promise<'timeout'>((resolve, _) => { setTimeout(resolve, 2500, "timeout")}); + const timeoutPromise = new Promise<'timeout'>((resolve) => { setTimeout(resolve, 2500, "timeout")}); const remoteLoadPromise = this.loadRemoteAccounts() // load remote accounts // race the two promises diff --git a/src/app/services/app-config.service.ts b/src/app/services/app-config.service.ts index 7c592d8..46f0663 100644 --- a/src/app/services/app-config.service.ts +++ b/src/app/services/app-config.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { LocalStorageService } from './local-storage.service'; import { environment } from 'src/environments/environment'; import { ENCRYPTION_OPTIONS_DEFAULT, ENCRYPTION_OPTIONS_KEY, ENCRYPTION_OPTIONS_PASSWORD_KEY, EncryptionOptions, LAST_PASSWORD_CHECK_KEY } from '../models/encryption-options.model'; +import { AppVersionInfo } from '../models/app-version.enum'; @Injectable({ providedIn: 'root' @@ -10,7 +11,7 @@ export class AppConfigService { constructor(private localStorage: LocalStorageService) { } - get versionInfo() { + get versionInfo(): AppVersionInfo { return environment.versionConfig; } diff --git a/src/app/services/authentication.service.ts b/src/app/services/authentication.service.ts index 70e8e4a..0ed7b93 100644 --- a/src/app/services/authentication.service.ts +++ b/src/app/services/authentication.service.ts @@ -2,7 +2,6 @@ import { inject, Injectable } from '@angular/core'; import { Auth, signInWithEmailAndPassword, signOut } from '@angular/fire/auth'; import { NavController } from '@ionic/angular'; import { LocalStorageService } from './local-storage.service'; -import { environment } from 'src/environments/environment'; import { AppConfigService } from './app-config.service'; @Injectable({ diff --git a/src/app/services/logging.service.ts b/src/app/services/logging.service.ts index 07c9568..bfcfa94 100644 --- a/src/app/services/logging.service.ts +++ b/src/app/services/logging.service.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { Injectable } from '@angular/core'; import { environment } from 'src/environments/environment'; @@ -5,16 +6,38 @@ import { environment } from 'src/environments/environment'; providedIn: 'root' }) export class LoggingService { + private systemConsole = { + log: console.log, + debug: console.debug, + warn: console.warn, + info: console.info + } + + private isLogEnabled = true; constructor() { } disableConsoleInProduction(): void { if(environment.production){ + this.isLogEnabled = false; console.warn(`🚨 Console output is disabled on production!`); - console.log = function():void{}; - console.debug = function():void{}; - console.warn = function():void{}; - console.info = function():void{}; + console.log = (...args: any[]): void => { + if(this.isLogEnabled) this.systemConsole.log(...args); + } + console.debug = (...args: any[]): void => { + if(this.isLogEnabled) this.systemConsole.debug(...args); + } + console.warn = (...args: any[]): void => { + if(this.isLogEnabled) this.systemConsole.warn(...args); + } + console.info = (...args: any[]): void => { + if(this.isLogEnabled) this.systemConsole.info(...args); + } } } + + enableConsole(): void { + this.isLogEnabled = true; + console.warn(`🚨 Console output is enabled!`); + } } diff --git a/src/app/services/logo.service.ts b/src/app/services/logo.service.ts index 8f49ce6..64b522e 100644 --- a/src/app/services/logo.service.ts +++ b/src/app/services/logo.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { environment } from 'src/environments/environment'; -import { BrandFetchSearchResultArray } from '../models/brand-fetch-search.model'; +import { BrandFetchSearchAPIResult, BrandFetchSearchResultArray } from '../models/brand-fetch-search.model'; @Injectable({ providedIn: 'root' @@ -43,7 +43,7 @@ export class LogoService { }) if(!response.ok) { throw new Error("Failed to fetch service info") } - const brands: BrandFetchSearchResultArray = (await response.json()).map((brand: any) => { + const brands: BrandFetchSearchResultArray = (await response.json()).map((brand: BrandFetchSearchAPIResult) => { return { brandId: brand.brandId, claimed: brand.claimed, diff --git a/src/app/services/migration.service.ts b/src/app/services/migration.service.ts index 92d1e1d..47ae14e 100644 --- a/src/app/services/migration.service.ts +++ b/src/app/services/migration.service.ts @@ -5,7 +5,6 @@ import { AppVersion } from '../models/app-version.enum'; import { VersionUtils } from '../utils/version-utils'; import { AppConfigService } from './app-config.service'; import { EncryptionOptions } from '../models/encryption-options.model'; -import { App } from '@capacitor/app'; @Injectable({ providedIn: 'root' @@ -25,7 +24,11 @@ export class MigrationService { console.log('Checking version:', {version, dataVersion, appVersion}) return version > dataVersion && version <= appVersion && version != AppVersion.UNKNOWN }) - .map(versionString => VersionUtils.appVersionFromVersionString(versionString)) + .map(versionString => { + const versionMapped = VersionUtils.appVersionFromVersionString(versionString) + console.log('Mapped version:', versionMapped) + return versionMapped + }) .sort(VersionUtils.appVersionCompare) if (migrationsToRun.length === 0) { diff --git a/src/app/utils/crypto-utils.ts b/src/app/utils/crypto-utils.ts index c4c0288..1b0c29a 100644 --- a/src/app/utils/crypto-utils.ts +++ b/src/app/utils/crypto-utils.ts @@ -15,7 +15,7 @@ export class CryptoUtils { throw new Error('Invalid length'); } - let buffer = new Uint8Array(length) + const buffer = new Uint8Array(length) if (window.crypto) { window.crypto.getRandomValues(buffer); // better random } else { // fallback to Math.random diff --git a/src/app/utils/global-utils.ts b/src/app/utils/global-utils.ts index 0f64af8..d3bebe0 100644 --- a/src/app/utils/global-utils.ts +++ b/src/app/utils/global-utils.ts @@ -2,21 +2,19 @@ export class GlobalUtils { static isMobile() { return window.innerWidth <= 768; } - + static hideSplashScreen(): Promise { - return new Promise((resolve, _) => { - setTimeout(() => { - let splash = document.getElementById('splash-container') - if (splash != null) { - splash.style.opacity = '0' - setTimeout(() => { - splash?.remove() - resolve() - }, 250); - } else { + return new Promise((resolve) => { + const splash = document.getElementById('splash-container') + if (splash != null) { + splash.style.opacity = '0' + setTimeout(() => { + splash?.remove() resolve() - } - }, 500); + }, 250); + } else { + resolve() + } }); } } diff --git a/src/app/utils/version-utils.ts b/src/app/utils/version-utils.ts index bf120f4..efdda39 100644 --- a/src/app/utils/version-utils.ts +++ b/src/app/utils/version-utils.ts @@ -8,17 +8,8 @@ export class VersionUtils { * @returns {AppVersion} The corresponding AppVersion enum value. */ static appVersionFromVersionString(version: string): AppVersion { - // switch (version) { - // case '1.0.0': - // return AppVersion.V1_0_0 - // case '2.0.0': - // return AppVersion.V2_0_0 - // case '2.1.0': - // return AppVersion.V2_1_0 - // default: - // return AppVersion.UNKNOWN - // } - return AppVersion[version as keyof typeof AppVersion] ?? AppVersion.UNKNOWN + const appVersion = Object.values(AppVersion).find((appVersion) => appVersion === version) + return appVersion as AppVersion ?? AppVersion.UNKNOWN } /** diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index e7fb342..c29addc 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -49,7 +49,8 @@ "ERROR_CAMERA_HEADER": "Camera unavailable", "ERROR_CAMERA_MESSAGE": "

Camera access is required to scan QR codes, but it seems your device either lacks a camera or camera permissions were denied.

Please check your device/permissions settings and try again.

", "INVALID_QR_CODE": "Invalid QR Code", - "INVALID_SESSION": "Invalid session" + "INVALID_SESSION": "Invalid session", + "INVALID_FIELDS": "Invalid account data" }, "EXAMPLI_GRATIA_SHORT": "e.g.", "LABEL": "Label", diff --git a/src/assets/i18n/pt.json b/src/assets/i18n/pt.json index 6f8f760..1fe61ab 100644 --- a/src/assets/i18n/pt.json +++ b/src/assets/i18n/pt.json @@ -49,7 +49,8 @@ "ERROR_CAMERA_HEADER": "Câmera não disponível", "ERROR_CAMERA_MESSAGE": "

O acesso à câmera é necessário para escanear QR codes, mas parece que seu dispositivo não possui uma câmera ou as permissões da câmera foram negadas.

Verifique as permissões e/ou configurações do seu dispositivo e tente novamente.

", "INVALID_QR_CODE": "QR Code inválido", - "INVALID_SESSION": "Sessão inválida" + "INVALID_SESSION": "Sessão inválida", + "INVALID_FIELDS": "Dados da conta inválidos" }, "EXAMPLI_GRATIA_SHORT": "ex.:", "LABEL": "Rótulo", diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 4307a02..459f5e2 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -16,7 +16,7 @@ export const environment = { messagingSenderId: "946698001868" }, versionConfig: { - versionNumber: "2.1.0", // set this to test migrations. On production build this will be the value from the package.json + versionNumber: "%VERSION%", // set this to test migrations. On production build this will be the value from the package.json buildDate: "%BUILD_DATE%", commitHash: "%COMMIT_HASH%", versionName: "%VERSION%-DEV"