Auth/PM-7077 - Browser Extension - UI Refresh - Account Switcher Component (#10268)

* PM-7077 - (1) Convert app-header to standalone (2) Convert account-switcher to standalone (3) Start wiring up extension refresh feature flag and fixing deps.

* PM-7077 WIP

* PM-7077 - Mostly get account switcher and account component converted to CL components.

* PM-7077 - Apply semibold classes to section headers to match storybook

* PM-7077 - AccountSwitcher - (1) Fix margin per design call (2) Add missing lockAll call

* PM-7077 - Remove test code.
This commit is contained in:
Jared Snider 2024-07-29 12:57:54 -04:00 committed by GitHub
parent 22bb1316cd
commit ea72552599
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 326 additions and 131 deletions

View File

@ -1,78 +1,177 @@
<app-header>
<div class="left">
<button type="button" (click)="back()">{{ "close" | i18n }}</button>
</div>
<div class="center tw-font-bold">{{ "switchAccounts" | i18n }}</div>
</app-header>
<ng-container *ngIf="extensionRefreshFlag">
<popup-page [loading]="loading">
<popup-header slot="header" pageTitle="{{ 'switchAccounts' | i18n }}" showBackButton>
<ng-container slot="end">
<app-pop-out></app-pop-out>
<app-current-account></app-current-account>
</ng-container>
</popup-header>
<main
*ngIf="loading"
class="tw-absolute tw-z-50 tw-box-border tw-flex tw-cursor-not-allowed tw-items-center tw-justify-center tw-bg-background tw-opacity-60"
>
<i class="bwi bwi-spinner bwi-2x bwi-spin" aria-hidden="true"></i>
</main>
<main>
<div class="tw-p-2">
<div *ngIf="availableAccounts$ | async as availableAccounts">
<ul class="tw-grid tw-list-none tw-gap-2" role="listbox">
<ng-container *ngIf="availableAccounts$ | async as availableAccounts">
<bit-section>
<ng-container *ngFor="let availableAccount of availableAccounts; first as isFirst">
<li *ngIf="availableAccount.isActive" class="tw-mb-4" role="option">
<auth-account [account]="availableAccount" (loading)="loading = $event"></auth-account>
</li>
<div *ngIf="isFirst" class="tw-uppercase tw-text-muted">
{{ "availableAccounts" | i18n }}
<div *ngIf="availableAccount.isActive" class="tw-mb-6">
<auth-account
[account]="availableAccount"
[extensionRefreshFlag]="extensionRefreshFlag"
(loading)="loading = $event"
></auth-account>
</div>
<bit-section-header *ngIf="isFirst">
<h2 bitTypography="h6" class="tw-font-semibold">{{ "availableAccounts" | i18n }}</h2>
</bit-section-header>
<div *ngIf="!availableAccount.isActive">
<auth-account
[account]="availableAccount"
[extensionRefreshFlag]="extensionRefreshFlag"
(loading)="loading = $event"
></auth-account>
</div>
<li *ngIf="!availableAccount.isActive" role="option">
<auth-account [account]="availableAccount" (loading)="loading = $event"></auth-account>
</li>
</ng-container>
</ul>
<!--
If the user has not reached the account limit, the last 'availableAccount' will have an 'id' of
'SPECIAL_ADD_ACCOUNT_ID'. Since we don't want to count this as one of the actual accounts,
we check to make sure the 'id' of the last 'availableAccount' is not equal to 'SPECIAL_ADD_ACCOUNT_ID'
-->
<p
class="tw-text-sm tw-text-muted"
*ngIf="
availableAccounts.length >= accountLimit &&
availableAccounts[availableAccounts.length - 1].id !== specialAddAccountId
"
>
{{ "accountLimitReached" | i18n }}
</p>
</div>
<!--
If the user has not reached the account limit, the last 'availableAccount' will have an 'id' of
'SPECIAL_ADD_ACCOUNT_ID'. Since we don't want to count this as one of the actual accounts,
we check to make sure the 'id' of the last 'availableAccount' is not equal to 'SPECIAL_ADD_ACCOUNT_ID'
-->
<p
class="tw-text-sm tw-text-muted"
*ngIf="
availableAccounts.length >= accountLimit &&
availableAccounts[availableAccounts.length - 1].id !== specialAddAccountId
"
>
{{ "accountLimitReached" | i18n }}
</p>
</bit-section>
</ng-container>
<div class="tw-mt-8" *ngIf="currentAccount$ | async as currentAccount">
<div class="tw-mb-2 tw-uppercase tw-text-muted">{{ "options" | i18n }}</div>
<div class="tw-grid tw-gap-2">
<button
type="button"
class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-p-3 disabled:tw-cursor-not-allowed disabled:tw-border-text-muted/60 disabled:!tw-text-muted/60"
(click)="lock(currentAccount.id)"
[disabled]="currentAccount.status === lockedStatus || !activeUserCanLock"
[title]="!activeUserCanLock ? ('unlockMethodNeeded' | i18n) : ''"
<bit-section>
<bit-section-header>
<h2 bitTypography="h6" class="tw-font-semibold">
{{ "options" | i18n }}
</h2>
</bit-section-header>
<bit-item>
<button
type="button"
bit-item-content
(click)="lock(currentAccount.id)"
[disabled]="currentAccount.status === lockedStatus || !activeUserCanLock"
[title]="!activeUserCanLock ? ('unlockMethodNeeded' | i18n) : ''"
>
<i slot="start" class="bwi bwi-lock tw-text-2xl tw-text-main" aria-hidden="true"></i>
{{ "lockNow" | i18n }}
</button>
</bit-item>
<bit-item>
<button type="button" bit-item-content (click)="logOut(currentAccount.id)">
<i
slot="start"
class="bwi bwi-sign-out tw-text-2xl tw-text-main"
aria-hidden="true"
></i>
{{ "logOut" | i18n }}
</button>
</bit-item>
<bit-item>
<button type="button" bit-item-content (click)="lockAll()">
<i slot="start" class="bwi bwi-lock tw-text-2xl tw-text-main" aria-hidden="true"></i>
{{ "lockAll" | i18n }}
</button>
</bit-item>
</bit-section>
</div>
</popup-page>
</ng-container>
<ng-container *ngIf="!extensionRefreshFlag">
<app-header>
<div class="left">
<button type="button" (click)="back()">{{ "close" | i18n }}</button>
</div>
<div class="center tw-font-bold">{{ "switchAccounts" | i18n }}</div>
</app-header>
<main
*ngIf="loading"
class="tw-absolute tw-z-50 tw-box-border tw-flex tw-cursor-not-allowed tw-items-center tw-justify-center tw-bg-background tw-opacity-60"
>
<i class="bwi bwi-spinner bwi-2x bwi-spin" aria-hidden="true"></i>
</main>
<main>
<div class="tw-p-2">
<div *ngIf="availableAccounts$ | async as availableAccounts">
<ul class="tw-grid tw-list-none tw-gap-2" role="listbox">
<ng-container *ngFor="let availableAccount of availableAccounts; first as isFirst">
<li *ngIf="availableAccount.isActive" class="tw-mb-4" role="option">
<auth-account
[account]="availableAccount"
(loading)="loading = $event"
></auth-account>
</li>
<div *ngIf="isFirst" class="tw-uppercase tw-text-muted">
{{ "availableAccounts" | i18n }}
</div>
<li *ngIf="!availableAccount.isActive" role="option">
<auth-account
[account]="availableAccount"
(loading)="loading = $event"
></auth-account>
</li>
</ng-container>
</ul>
<!--
If the user has not reached the account limit, the last 'availableAccount' will have an 'id' of
'SPECIAL_ADD_ACCOUNT_ID'. Since we don't want to count this as one of the actual accounts,
we check to make sure the 'id' of the last 'availableAccount' is not equal to 'SPECIAL_ADD_ACCOUNT_ID'
-->
<p
class="tw-text-sm tw-text-muted"
*ngIf="
availableAccounts.length >= accountLimit &&
availableAccounts[availableAccounts.length - 1].id !== specialAddAccountId
"
>
<i class="bwi bwi-lock tw-text-2xl" aria-hidden="true"></i>
{{ "lockNow" | i18n }}
</button>
<button
type="button"
class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-p-3"
(click)="logOut(currentAccount.id)"
>
<i class="bwi bwi-sign-out tw-text-2xl" aria-hidden="true"></i>
{{ "logOut" | i18n }}
</button>
<button
type="button"
class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-p-3"
(click)="lockAll()"
>
<i class="bwi bwi-lock tw-text-2xl" aria-hidden="true"></i>
{{ "lockAll" | i18n }}
</button>
{{ "accountLimitReached" | i18n }}
</p>
</div>
<div class="tw-mt-8" *ngIf="currentAccount$ | async as currentAccount">
<div class="tw-mb-2 tw-uppercase tw-text-muted">{{ "options" | i18n }}</div>
<div class="tw-grid tw-gap-2">
<button
type="button"
class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-p-3 disabled:tw-cursor-not-allowed disabled:tw-border-text-muted/60 disabled:!tw-text-muted/60"
(click)="lock(currentAccount.id)"
[disabled]="currentAccount.status === lockedStatus || !activeUserCanLock"
[title]="!activeUserCanLock ? ('unlockMethodNeeded' | i18n) : ''"
>
<i class="bwi bwi-lock tw-text-2xl" aria-hidden="true"></i>
{{ "lockNow" | i18n }}
</button>
<button
type="button"
class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-p-3"
(click)="logOut(currentAccount.id)"
>
<i class="bwi bwi-sign-out tw-text-2xl" aria-hidden="true"></i>
{{ "logOut" | i18n }}
</button>
<button
type="button"
class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-p-3"
(click)="lockAll()"
>
<i class="bwi bwi-lock tw-text-2xl" aria-hidden="true"></i>
{{ "lockAll" | i18n }}
</button>
</div>
</div>
</div>
</div>
</main>
</main>
</ng-container>

View File

@ -1,22 +1,54 @@
import { Location } from "@angular/common";
import { CommonModule, Location } from "@angular/common";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { Subject, firstValueFrom, map, of, switchMap, takeUntil } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { UserId } from "@bitwarden/common/types/guid";
import { DialogService } from "@bitwarden/components";
import {
AvatarModule,
ButtonModule,
DialogService,
ItemModule,
SectionComponent,
SectionHeaderComponent,
} from "@bitwarden/components";
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
import { HeaderComponent } from "../../../platform/popup/header.component";
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
import { AccountComponent } from "./account.component";
import { CurrentAccountComponent } from "./current-account.component";
import { AccountSwitcherService } from "./services/account-switcher.service";
@Component({
standalone: true,
templateUrl: "account-switcher.component.html",
imports: [
CommonModule,
JslibModule,
ButtonModule,
ItemModule,
AvatarModule,
PopupPageComponent,
PopupHeaderComponent,
HeaderComponent,
PopOutComponent,
CurrentAccountComponent,
AccountComponent,
SectionComponent,
SectionHeaderComponent,
],
})
export class AccountSwitcherComponent implements OnInit, OnDestroy {
readonly lockedStatus = AuthenticationStatus.Locked;
@ -24,17 +56,18 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
loading = false;
activeUserCanLock = false;
extensionRefreshFlag = false;
constructor(
private accountSwitcherService: AccountSwitcherService,
private accountService: AccountService,
private vaultTimeoutService: VaultTimeoutService,
private messagingService: MessagingService,
private dialogService: DialogService,
private location: Location,
private router: Router,
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
private authService: AuthService,
private configService: ConfigService,
) {}
get accountLimit() {
@ -55,6 +88,10 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
);
async ngOnInit() {
this.extensionRefreshFlag = await this.configService.getFeatureFlag(
FeatureFlag.ExtensionRefresh,
);
const availableVaultTimeoutActions = await firstValueFrom(
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(),
);

View File

@ -1,54 +1,109 @@
<button
*ngIf="account.id !== specialAccountAddId"
type="button"
class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-border-none tw-p-3"
(click)="selectAccount(account.id)"
>
<div class="tw-flex-shrink-0">
<bit-avatar
[id]="account.id"
[text]="account.name"
[color]="account.avatarColor"
size="small"
aria-hidden="true"
></bit-avatar>
</div>
<div class="tw-text-left">
<span class="tw-sr-only" *ngIf="status.text === 'active'"> {{ "activeAccount" | i18n }}: </span>
<span class="tw-sr-only" *ngIf="status.text !== 'active'">
{{ "switchToAccount" | i18n }}
</span>
<div class="tw-max-w-64 tw-truncate">
{{ account.email }}
</div>
<div class="account-switcher-row-details tw-max-w-64 tw-truncate tw-text-sm">
<span class="tw-sr-only">{{ "hostedAt" | i18n }}</span>
{{ account.server }}
</div>
<div
class="account-switcher-row-details tw-text-sm tw-italic"
[attr.aria-hidden]="status.text === 'active'"
>
<span class="tw-sr-only">(</span>
<span [ngClass]="status.text === 'active' ? 'tw-font-bold tw-text-success' : ''">{{
status.text
}}</span>
<span class="tw-sr-only">)</span>
</div>
</div>
<div class="tw-ml-auto tw-flex-shrink-0">
<i class="bwi tw-text-2xl" [ngClass]="status.icon" aria-hidden="true"></i>
</div>
</button>
<ng-container *ngIf="extensionRefreshFlag">
<bit-item *ngIf="account.id !== specialAccountAddId">
<button bit-item-content type="button" (click)="selectAccount(account.id)">
<bit-avatar
slot="start"
[id]="account.id"
[text]="account.name"
[color]="account.avatarColor"
size="small"
aria-hidden="true"
></bit-avatar>
<button
*ngIf="account.id === specialAccountAddId"
type="button"
class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-border-none tw-p-3"
(click)="selectAccount(account.id)"
>
<i class="bwi bwi-plus tw-text-2xl" aria-hidden="true"></i>
<div>
{{ account.name | i18n }}
</div>
</button>
<span class="tw-sr-only" *ngIf="status.text === 'active'">
{{ "activeAccount" | i18n }}:
</span>
<span class="tw-sr-only" *ngIf="status.text !== 'active'">
{{ "switchToAccount" | i18n }}
</span>
<div class="tw-max-w-64 tw-truncate">
{{ account.email }}
</div>
<ng-container slot="secondary">
<div class="tw-max-w-64 tw-truncate tw-text-sm">
<span class="tw-sr-only">{{ "hostedAt" | i18n }}</span>
{{ account.server }}
</div>
<div class="tw-text-sm tw-italic" [attr.aria-hidden]="status.text === 'active'">
<span class="tw-sr-only">(</span>
<span [ngClass]="status.text === 'active' ? 'tw-font-bold tw-text-success' : ''">{{
status.text
}}</span>
<span class="tw-sr-only">)</span>
</div>
</ng-container>
<i slot="end" class="bwi tw-text-2xl" [ngClass]="status.icon" aria-hidden="true"></i>
</button>
</bit-item>
<bit-item *ngIf="account.id === specialAccountAddId">
<button type="button" bit-item-content (click)="selectAccount(account.id)">
<i slot="start" class="bwi bwi-plus tw-text-2xl tw-text-main" aria-hidden="true"></i>
{{ account.name | i18n }}
</button>
</bit-item>
</ng-container>
<ng-container *ngIf="!extensionRefreshFlag">
<button
*ngIf="account.id !== specialAccountAddId"
type="button"
class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-border-none tw-p-3"
(click)="selectAccount(account.id)"
>
<div class="tw-flex-shrink-0">
<bit-avatar
[id]="account.id"
[text]="account.name"
[color]="account.avatarColor"
size="small"
aria-hidden="true"
></bit-avatar>
</div>
<div class="tw-text-left">
<span class="tw-sr-only" *ngIf="status.text === 'active'">
{{ "activeAccount" | i18n }}:
</span>
<span class="tw-sr-only" *ngIf="status.text !== 'active'">
{{ "switchToAccount" | i18n }}
</span>
<div class="tw-max-w-64 tw-truncate">
{{ account.email }}
</div>
<div class="account-switcher-row-details tw-max-w-64 tw-truncate tw-text-sm">
<span class="tw-sr-only">{{ "hostedAt" | i18n }}</span>
{{ account.server }}
</div>
<div
class="account-switcher-row-details tw-text-sm tw-italic"
[attr.aria-hidden]="status.text === 'active'"
>
<span class="tw-sr-only">(</span>
<span [ngClass]="status.text === 'active' ? 'tw-font-bold tw-text-success' : ''">{{
status.text
}}</span>
<span class="tw-sr-only">)</span>
</div>
</div>
<div class="tw-ml-auto tw-flex-shrink-0">
<i class="bwi tw-text-2xl" [ngClass]="status.icon" aria-hidden="true"></i>
</div>
</button>
<button
*ngIf="account.id === specialAccountAddId"
type="button"
class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-border-none tw-p-3"
(click)="selectAccount(account.id)"
>
<i class="bwi bwi-plus tw-text-2xl" aria-hidden="true"></i>
<div>
{{ account.name | i18n }}
</div>
</button>
</ng-container>

View File

@ -5,7 +5,7 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { AvatarModule } from "@bitwarden/components";
import { AvatarModule, ItemModule } from "@bitwarden/components";
import { AccountSwitcherService, AvailableAccount } from "./services/account-switcher.service";
@ -13,10 +13,11 @@ import { AccountSwitcherService, AvailableAccount } from "./services/account-swi
standalone: true,
selector: "auth-account",
templateUrl: "account.component.html",
imports: [CommonModule, JslibModule, AvatarModule],
imports: [CommonModule, JslibModule, AvatarModule, ItemModule],
})
export class AccountComponent {
@Input() account: AvailableAccount;
@Input() extensionRefreshFlag: boolean = false;
@Output() loading = new EventEmitter<boolean>();
constructor(

View File

@ -1,14 +1,18 @@
import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core";
import { Observable, map, of, switchMap } from "rxjs";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { CurrentAccountComponent } from "../../auth/popup/account-switching/current-account.component";
import { enableAccountSwitching } from "../flags";
@Component({
selector: "app-header",
templateUrl: "header.component.html",
standalone: true,
imports: [CommonModule, CurrentAccountComponent],
})
export class HeaderComponent {
@Input() noTheme = false;

View File

@ -17,7 +17,6 @@ import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe"
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
import { AvatarModule, ButtonModule, ToastModule } from "@bitwarden/components";
import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component";
import { AccountComponent } from "../auth/popup/account-switching/account.component";
import { CurrentAccountComponent } from "../auth/popup/account-switching/current-account.component";
import { EnvironmentComponent } from "../auth/popup/environment.component";
@ -123,6 +122,7 @@ import "../platform/popup/locales";
PopupTabNavigationComponent,
PopupFooterComponent,
PopupHeaderComponent,
HeaderComponent,
UserVerificationDialogComponent,
CurrentAccountComponent,
],
@ -145,7 +145,6 @@ import "../platform/popup/locales";
FolderAddEditComponent,
FoldersComponent,
VaultFilterComponent,
HeaderComponent,
HintComponent,
HomeComponent,
LockComponent,
@ -184,8 +183,8 @@ import "../platform/popup/locales";
Fido2Component,
AutofillV1Component,
EnvironmentSelectorComponent,
AccountSwitcherComponent,
],
exports: [],
providers: [CurrencyPipe, DatePipe],
bootstrap: [AppComponent],
})