-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
moved add account modal logic to its component
- Loading branch information
Showing
10 changed files
with
369 additions
and
339 deletions.
There are no files selected for viewing
89 changes: 86 additions & 3 deletions
89
src/app/home/components/account-modal/account-modal.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,86 @@ | ||
<p> | ||
account-modal works! | ||
</p> | ||
<ion-header> | ||
<ion-toolbar> | ||
<ion-title>{{ "ADD_ACCOUNT_MODAL.TITLE" | translate }}</ion-title> | ||
<ion-buttons slot="end"> | ||
<ion-button (click)="dismiss()"><ion-icon slot="icon-only" name="close-outline"></ion-icon></ion-button> | ||
</ion-buttons> | ||
</ion-toolbar> | ||
</ion-header> | ||
<ion-content class="ion-padding"> | ||
<ng-container *ngIf="isScanActive;else manualForm"> | ||
<ngx-scanner-qrcode #qrscanner="scanner" (event)="onQRCodeScanned($event, qrscanner)" | ||
[config]="qrScannerOpts"></ngx-scanner-qrcode> | ||
<ion-grid> | ||
<ion-row> | ||
<ion-col size="auto"> | ||
</ion-col> | ||
<ion-col></ion-col> | ||
<ion-col size="auto"> | ||
<ion-button size="large" (click)="cycleCamera()"> | ||
<ion-icon slot="icon-only" name="sync-outline"></ion-icon> | ||
</ion-button> | ||
</ion-col> | ||
</ion-row> | ||
<ion-row> | ||
<ion-col size="12"> | ||
<ion-button size="large" expand="block" (click)="manualInputAction()"> | ||
<ion-icon slot="start" name="create-outline"></ion-icon> | ||
{{ "ADD_ACCOUNT_MODAL.MANUAL_INPUT" | translate }} | ||
</ion-button> | ||
</ion-col> | ||
</ion-row> | ||
</ion-grid> | ||
</ng-container> | ||
<ng-template #manualForm> | ||
<ion-button size="large" expand="block" (click)="scanCode()"> | ||
<ion-icon slot="start" name="qr-code-outline"></ion-icon> | ||
{{ "ADD_ACCOUNT_MODAL.SCAN_QR_CODE" | translate }} | ||
</ion-button> | ||
<form [formGroup]="validations_form" (ngSubmit)="createAccount(validations_form.value)"> | ||
<ion-item> | ||
<ion-input label="{{ 'ADD_ACCOUNT_MODAL.LABEL' | translate }}" labelPlacement="stacked" formControlName="label" | ||
placeholder="{{ 'ADD_ACCOUNT_MODAL.EXAMPLI_GRATIA_SHORT' | translate }} Google:teste@gmail.com"></ion-input> | ||
</ion-item> | ||
<ion-item> | ||
<ion-input label="{{ 'ADD_ACCOUNT_MODAL.SECRET_KEY' | translate }}" labelPlacement="stacked" | ||
formControlName="secret" | ||
placeholder="{{ 'ADD_ACCOUNT_MODAL.EXAMPLI_GRATIA_SHORT' | translate }} QAPERTPEO123"></ion-input> | ||
</ion-item> | ||
<ion-item> | ||
<ion-input label="{{ 'ADD_ACCOUNT_MODAL.TOKEN_SIZE' | translate }}" labelPlacement="stacked" | ||
formControlName="tokenLength" | ||
placeholder="{{ 'ADD_ACCOUNT_MODAL.EXAMPLI_GRATIA_SHORT' | translate }} 6"></ion-input> | ||
</ion-item> | ||
<ion-item> | ||
<ion-input label="{{ 'ADD_ACCOUNT_MODAL.TOKEN_INTERVAL' | translate }}" labelPlacement="stacked" | ||
formControlName="interval" | ||
placeholder="{{ 'ADD_ACCOUNT_MODAL.EXAMPLI_GRATIA_SHORT' | translate }} 30"></ion-input> | ||
</ion-item> | ||
<ion-item> | ||
<ion-label position="stacked">{{ "ADD_ACCOUNT_MODAL.LOGO" | translate }}</ion-label> | ||
<ion-searchbar placeholder="{{ 'SEARCH' | translate }}" class="ion-padding-top" [debounce]="250" | ||
[value]="draftLogoSearchTxt" (ionInput)="handleSearchLogo($event)"></ion-searchbar> | ||
<ion-grid class="full-row"> | ||
<ion-row class="full-row"> | ||
<ion-col size-xs="6" size-sm="4" size="4"> | ||
<ion-card class="logo-card" (click)="selectLogo('')" [ngClass]="{ 'selected-logo': !draftLogoURL }"> | ||
<ion-img class="ion-padding logo-img" [src]="'assets/icon/favicon.png'"></ion-img> | ||
<p>{{ "ADD_ACCOUNT_MODAL.NO_LOGO" | translate }}</p> | ||
</ion-card> | ||
</ion-col> | ||
<ion-col size-xs="6" size-sm="4" *ngFor="let logoResult of searchLogoResults"> | ||
<ion-card class="logo-card" (click)="selectLogo(logoResult)" | ||
[ngClass]="{ 'selected-logo': logoResult === draftLogoURL }"> | ||
<ion-img class="ion-padding ion-margin logo-img" [src]="logoResult"></ion-img> | ||
</ion-card> | ||
</ion-col> | ||
</ion-row> | ||
</ion-grid> | ||
<p *ngIf="searchLogoResults.length > 0">Logos provided by <a href="https://brandfetch.com">Brandfetch</a> | ||
</p> | ||
</ion-item> | ||
<ion-button expand="block" [disabled]="!validations_form.valid" type="submit">{{ "ADD_ACCOUNT_MODAL.SAVE" | | ||
translate }}</ion-button> | ||
</form> | ||
</ng-template> | ||
</ion-content> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
24 changes: 0 additions & 24 deletions
24
src/app/home/components/account-modal/account-modal.component.spec.ts
This file was deleted.
Oops, something went wrong.
244 changes: 240 additions & 4 deletions
244
src/app/home/components/account-modal/account-modal.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,250 @@ | ||
import { Component, OnInit } from '@angular/core'; | ||
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 { | ||
export class AccountModalComponent implements OnInit { | ||
@ViewChild('qrscanner') qrscanner!: NgxScannerQrcodeComponent; | ||
|
||
constructor() { } | ||
isScanActive = false; | ||
draftLogoURL = ''; | ||
draftLogoSearchTxt = ''; | ||
searchLogoResults: string[] = []; | ||
|
||
ngOnInit() {} | ||
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.qrscanner.stop() | ||
} | ||
} | ||
|
||
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.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() { | ||
const current = this.qrscanner.deviceIndexActive | ||
const devices = await firstValueFrom(this.qrscanner.devices) | ||
console.log({ }) | ||
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) | ||
} | ||
|
||
manualInputAction() { | ||
if (this.qrscanner) { | ||
console.log("STOP QR Reading") | ||
this.qrscanner.stop() | ||
} | ||
this.isScanActive = false; | ||
} | ||
|
||
async scanCode() { | ||
// <ngx-scanner-qrcode #action="scanner" (event)="onEvent($event, action)"></ngx-scanner-qrcode> | ||
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()) | ||
} 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.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) { | ||
const toast = await this.toastController.create({ | ||
message, | ||
duration: 2000, | ||
}) | ||
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<void> { | ||
// 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<void> { | ||
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() | ||
} | ||
} |
Oops, something went wrong.