From 7446f1c68023e06d00302ca4c1d4edc4b3f154da Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Wed, 10 May 2023 10:48:16 +0200 Subject: [PATCH] [PM-2014] feat: improve async data loading --- .../services/webauthn/webauthn.service.ts | 27 +++++++++--- .../fido2-login-settings.component.html | 1 + .../fido2-login-settings.component.ts | 41 ++++++++++++------- 3 files changed, 48 insertions(+), 21 deletions(-) diff --git a/apps/web/src/app/auth/core/services/webauthn/webauthn.service.ts b/apps/web/src/app/auth/core/services/webauthn/webauthn.service.ts index 717cd8cdc9..80a580d4d6 100644 --- a/apps/web/src/app/auth/core/services/webauthn/webauthn.service.ts +++ b/apps/web/src/app/auth/core/services/webauthn/webauthn.service.ts @@ -1,5 +1,5 @@ import { Injectable, Optional } from "@angular/core"; -import { from, map, Observable } from "rxjs"; +import { BehaviorSubject, from, map, Observable, shareReplay, switchMap, tap } from "rxjs"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; @@ -17,17 +17,26 @@ import { WebauthnApiService } from "./webauthn-api.service"; @Injectable({ providedIn: CoreAuthModule }) export class WebauthnService { - private credentials: CredentialsContainer; + private navigatorCredentials: CredentialsContainer; + private _refresh$ = new BehaviorSubject(undefined); + private _loading$ = new BehaviorSubject(true); + + readonly credentials$ = this._refresh$.pipe( + switchMap(() => this.getCredentials$()), + tap(() => this._loading$.next(false)), + shareReplay({ bufferSize: 1, refCount: true }) + ); + readonly loading$ = this._loading$.asObservable(); constructor( private apiService: WebauthnApiService, private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, - @Optional() credentials?: CredentialsContainer, + @Optional() navigatorCredentials?: CredentialsContainer, @Optional() private logService?: LogService ) { // Default parameters don't work when used with Angular DI - this.credentials = credentials ?? navigator.credentials; + this.navigatorCredentials = navigatorCredentials ?? navigator.credentials; } async getCredentialCreateOptions( @@ -59,7 +68,7 @@ export class WebauthnService { }; try { - const response = await this.credentials.create(nativeOptions); + const response = await this.navigatorCredentials.create(nativeOptions); if (!(response instanceof PublicKeyCredential)) { return undefined; } @@ -81,6 +90,7 @@ export class WebauthnService { request.token = credentialOptions.token; request.name = name; await this.apiService.saveCredential(request); + this.refresh(); return true; } catch (error) { this.logService?.error(error); @@ -89,7 +99,12 @@ export class WebauthnService { } } - getCredentials$(): Observable { + private getCredentials$(): Observable { return from(this.apiService.getCredentials()).pipe(map((response) => response.data)); } + + private refresh() { + this._loading$.next(true); + this._refresh$.next(); + } } diff --git a/apps/web/src/app/auth/settings/fido2-login-settings/fido2-login-settings.component.html b/apps/web/src/app/auth/settings/fido2-login-settings/fido2-login-settings.component.html index 07dc7c12c6..866e67c5bf 100644 --- a/apps/web/src/app/auth/settings/fido2-login-settings/fido2-login-settings.component.html +++ b/apps/web/src/app/auth/settings/fido2-login-settings/fido2-login-settings.component.html @@ -1,5 +1,6 @@

{{ "loginWithPasskey" | i18n }} Not implemented +

{{ "loginWithPasskeyInfo" | i18n }} diff --git a/apps/web/src/app/auth/settings/fido2-login-settings/fido2-login-settings.component.ts b/apps/web/src/app/auth/settings/fido2-login-settings/fido2-login-settings.component.ts index 16e8544a3c..fd10a46203 100644 --- a/apps/web/src/app/auth/settings/fido2-login-settings/fido2-login-settings.component.ts +++ b/apps/web/src/app/auth/settings/fido2-login-settings/fido2-login-settings.component.ts @@ -1,39 +1,50 @@ -import { Component, OnInit } from "@angular/core"; -import { firstValueFrom, Observable } from "rxjs"; +import { Component, HostBinding, OnDestroy, OnInit } from "@angular/core"; +import { Observable, Subject, takeUntil } from "rxjs"; import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog"; import { WebauthnService } from "../../core"; import { WebauthnCredentialView } from "../../core/views/webauth-credential.view"; -import { - CreateCredentialDialogResult, - openCreateCredentialDialog, -} from "./create-credential-dialog/create-credential-dialog.component"; +import { openCreateCredentialDialog } from "./create-credential-dialog/create-credential-dialog.component"; @Component({ selector: "app-fido2-login-settings", templateUrl: "fido2-login-settings.component.html", + host: { + "aria-live": "polite", + }, }) -export class Fido2LoginSettingsComponent implements OnInit { +export class Fido2LoginSettingsComponent implements OnInit, OnDestroy { + private destroy$ = new Subject(); + protected credentials$: Observable; + protected loading = true; constructor( private webauthnService: WebauthnService, private dialogService: DialogServiceAbstraction ) {} + @HostBinding("attr.aria-busy") + get ariaBusy() { + return this.loading ? "true" : "false"; + } + ngOnInit(): void { - this.credentials$ = this.webauthnService.getCredentials$(); + this.credentials$ = this.webauthnService.credentials$; + + this.webauthnService.loading$ + .pipe(takeUntil(this.destroy$)) + .subscribe((loading) => (this.loading = loading)); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); } protected async createCredential() { - const dialogRef = openCreateCredentialDialog(this.dialogService, {}); - - const result = await firstValueFrom(dialogRef.closed); - - if (result === CreateCredentialDialogResult.Success) { - /** Refresh */ - } + openCreateCredentialDialog(this.dialogService, {}); } }