mirror of
https://github.com/bitwarden/browser
synced 2026-03-02 11:31:44 +00:00
[PM 29776] Move Account Switcher (#18686)
* moved account switcher into page header * Added 'header-actions' slot for AnonLayout * removed 'standalone: true' * cleanup * more cleanup * fixed lint error * use 'app-avatar' in dropdown * fixed account avatar vertical centering * revert back to `standalone: false` * moved new account switcher behind 'desktop-ui-migration-milestone-4' FF * - added to default FF - fixed template formatting - removed type of 'configService' - renamed to 'account-switcher-v2' * fixed test * moved 'account-switcher-v2' into '/auth' * updated class and style from 'v3' to 'v2' * removed ts-strict-ignore * added back `ts-strict-ignore` * added `aria-label` to `bit-avatar` * fixed page load issue after clearing local session data * fixed 'Access denied' toast appearing when milestone4 FF is false * fixed new account switcher missing from 'locked vaults' and other 'AnonLayouts' * fixed styles for non-logged in account-switcher button * fixed duplicate account switcher bug * lint fix * updated 'account switcher' anon layout styles/colors * fixed color from `bg-brand-strong` to `bg-brand` --------- Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>
This commit is contained in:
@@ -48,6 +48,7 @@ import {
|
||||
RemovePasswordComponent,
|
||||
} from "@bitwarden/key-management-ui";
|
||||
|
||||
import { AccountSwitcherV2Component } from "../auth/components/account-switcher/account-switcher-v2.component";
|
||||
import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard";
|
||||
import { reactiveUnlockVaultGuard } from "../autofill/guards/reactive-vault-guard";
|
||||
import { Fido2CreateComponent } from "../autofill/modal/credentials/fido2-create.component";
|
||||
@@ -84,6 +85,11 @@ const routes: Routes = [
|
||||
path: "",
|
||||
component: AuthenticationTimeoutComponent,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: AccountSwitcherV2Component,
|
||||
outlet: "header-actions",
|
||||
},
|
||||
],
|
||||
data: {
|
||||
pageIcon: TwoFactorTimeoutIcon,
|
||||
@@ -96,7 +102,14 @@ const routes: Routes = [
|
||||
path: AuthRoute.NewDeviceVerification,
|
||||
component: AnonLayoutWrapperComponent,
|
||||
canActivate: [unauthGuardFn(), activeAuthGuard()],
|
||||
children: [{ path: "", component: NewDeviceVerificationComponent }],
|
||||
children: [
|
||||
{ path: "", component: NewDeviceVerificationComponent },
|
||||
{
|
||||
path: "",
|
||||
component: AccountSwitcherV2Component,
|
||||
outlet: "header-actions",
|
||||
},
|
||||
],
|
||||
data: {
|
||||
pageIcon: TwoFactorAuthEmailIcon,
|
||||
pageTitle: {
|
||||
@@ -160,6 +173,11 @@ const routes: Routes = [
|
||||
loginRoute: `/${AuthRoute.Login}`,
|
||||
} satisfies RegistrationStartSecondaryComponentData,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: AccountSwitcherV2Component,
|
||||
outlet: "header-actions",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -173,6 +191,11 @@ const routes: Routes = [
|
||||
path: "",
|
||||
component: RegistrationFinishComponent,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: AccountSwitcherV2Component,
|
||||
outlet: "header-actions",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -192,6 +215,11 @@ const routes: Routes = [
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: AccountSwitcherV2Component,
|
||||
outlet: "header-actions",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -200,7 +228,14 @@ const routes: Routes = [
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
},
|
||||
children: [{ path: "", component: LoginDecryptionOptionsComponent }],
|
||||
children: [
|
||||
{ path: "", component: LoginDecryptionOptionsComponent },
|
||||
{
|
||||
path: "",
|
||||
component: AccountSwitcherV2Component,
|
||||
outlet: "header-actions",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: AuthRoute.Sso,
|
||||
@@ -220,6 +255,11 @@ const routes: Routes = [
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: AccountSwitcherV2Component,
|
||||
outlet: "header-actions",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -240,6 +280,11 @@ const routes: Routes = [
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: AccountSwitcherV2Component,
|
||||
outlet: "header-actions",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -253,7 +298,14 @@ const routes: Routes = [
|
||||
key: "adminApprovalRequestSentToAdmins",
|
||||
},
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
children: [{ path: "", component: LoginViaAuthRequestComponent }],
|
||||
children: [
|
||||
{ path: "", component: LoginViaAuthRequestComponent },
|
||||
{
|
||||
path: "",
|
||||
component: AccountSwitcherV2Component,
|
||||
outlet: "header-actions",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: AuthRoute.PasswordHint,
|
||||
@@ -274,6 +326,11 @@ const routes: Routes = [
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: AccountSwitcherV2Component,
|
||||
outlet: "header-actions",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -291,6 +348,11 @@ const routes: Routes = [
|
||||
path: "",
|
||||
component: LockComponent,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: AccountSwitcherV2Component,
|
||||
outlet: "header-actions",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -301,6 +363,11 @@ const routes: Routes = [
|
||||
path: "",
|
||||
component: TwoFactorAuthComponent,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: AccountSwitcherV2Component,
|
||||
outlet: "header-actions",
|
||||
},
|
||||
],
|
||||
data: {
|
||||
pageTitle: {
|
||||
@@ -313,23 +380,42 @@ const routes: Routes = [
|
||||
{
|
||||
path: AuthRoute.SetInitialPassword,
|
||||
canActivate: [authGuard],
|
||||
component: SetInitialPasswordComponent,
|
||||
data: {
|
||||
maxWidth: "lg",
|
||||
pageIcon: LockIcon,
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: SetInitialPasswordComponent,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: AccountSwitcherV2Component,
|
||||
outlet: "header-actions",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: AuthRoute.ChangePassword,
|
||||
component: ChangePasswordComponent,
|
||||
canActivate: [authGuard],
|
||||
data: {
|
||||
pageIcon: LockIcon,
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: ChangePasswordComponent,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: AccountSwitcherV2Component,
|
||||
outlet: "header-actions",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "remove-password",
|
||||
component: RemovePasswordComponent,
|
||||
canActivate: [authGuard],
|
||||
data: {
|
||||
pageTitle: {
|
||||
@@ -337,10 +423,20 @@ const routes: Routes = [
|
||||
},
|
||||
pageIcon: LockIcon,
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: RemovePasswordComponent,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: AccountSwitcherV2Component,
|
||||
outlet: "header-actions",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "confirm-key-connector-domain",
|
||||
component: ConfirmKeyConnectorDomainComponent,
|
||||
canActivate: [],
|
||||
data: {
|
||||
pageTitle: {
|
||||
@@ -348,6 +444,17 @@ const routes: Routes = [
|
||||
},
|
||||
pageIcon: DomainIcon,
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: ConfirmKeyConnectorDomainComponent,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: AccountSwitcherV2Component,
|
||||
outlet: "header-actions",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -491,9 +491,10 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
this.messagingService.send("unlocked");
|
||||
this.loading = true;
|
||||
await this.syncService.fullSync(false);
|
||||
this.loading = false;
|
||||
// Force reload to ensure route guards are activated
|
||||
await this.router.navigate(["vault"], { onSameUrlNavigation: "reload" });
|
||||
// Clear loading after navigating to avoid flickering the previous route
|
||||
this.loading = false;
|
||||
}
|
||||
this.messagingService.send("finishSwitchAccount");
|
||||
break;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<div class="header">
|
||||
<app-search></app-search>
|
||||
<app-account-switcher></app-account-switcher>
|
||||
|
||||
@if (!useNewAccountSwitcher()) {
|
||||
<app-account-switcher></app-account-switcher>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Component, inject } from "@angular/core";
|
||||
import { toSignal } from "@angular/core/rxjs-interop";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@@ -7,4 +11,11 @@ import { Component } from "@angular/core";
|
||||
templateUrl: "header.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class HeaderComponent {}
|
||||
export class HeaderComponent {
|
||||
private configService: ConfigService = inject(ConfigService);
|
||||
|
||||
protected readonly useNewAccountSwitcher = toSignal(
|
||||
this.configService.getFeatureFlag$(FeatureFlag.DesktopUiMigrationMilestone4),
|
||||
{ initialValue: false },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,4 +16,6 @@
|
||||
<ng-container slot="tabs">
|
||||
<ng-content select="[slot=tabs]" />
|
||||
</ng-container>
|
||||
|
||||
<app-account-switcher-v2 />
|
||||
</bit-header>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { mock } from "jest-mock-extended";
|
||||
@@ -6,6 +7,8 @@ import { of } from "rxjs";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { HeaderComponent } from "@bitwarden/components";
|
||||
|
||||
import { AccountSwitcherV2Component } from "../../../auth/components/account-switcher/account-switcher-v2.component";
|
||||
|
||||
import { DesktopHeaderComponent } from "./desktop-header.component";
|
||||
|
||||
describe("DesktopHeaderComponent", () => {
|
||||
@@ -34,7 +37,12 @@ describe("DesktopHeaderComponent", () => {
|
||||
useValue: mockActivatedRoute,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
})
|
||||
.overrideComponent(DesktopHeaderComponent, {
|
||||
remove: { imports: [AccountSwitcherV2Component] },
|
||||
add: { schemas: [CUSTOM_ELEMENTS_SCHEMA] },
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(DesktopHeaderComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
@@ -6,10 +6,12 @@ import { map } from "rxjs";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { HeaderComponent, BannerModule } from "@bitwarden/components";
|
||||
|
||||
import { AccountSwitcherV2Component } from "../../../auth/components/account-switcher/account-switcher-v2.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-header",
|
||||
templateUrl: "./desktop-header.component.html",
|
||||
imports: [BannerModule, HeaderComponent],
|
||||
imports: [BannerModule, HeaderComponent, AccountSwitcherV2Component],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DesktopHeaderComponent {
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
<ng-container *ngIf="view$ | async as view">
|
||||
<div
|
||||
cdkOverlayOrigin
|
||||
#trigger="cdkOverlayOrigin"
|
||||
[hidden]="!view.showSwitcher || !useNewAccountSwitcher()"
|
||||
>
|
||||
@if (view.activeAccount?.email != null) {
|
||||
<button
|
||||
class="account-switcher-v2"
|
||||
type="button"
|
||||
(click)="toggle()"
|
||||
[disabled]="disabled"
|
||||
aria-haspopup="dialog"
|
||||
>
|
||||
<bit-avatar
|
||||
[text]="view.activeAccount.name ?? view.activeAccount.email"
|
||||
[id]="view.activeAccount.id"
|
||||
[color]="view.activeAccount.avatarColor"
|
||||
[attr.aria-label]="view.activeAccount.name ?? view.activeAccount.email"
|
||||
></bit-avatar>
|
||||
</button>
|
||||
} @else {
|
||||
<div class="tw-rounded-full tw-bg-bg-brand">
|
||||
<button
|
||||
class="account-switcher-v2 !tw-rounded-full"
|
||||
type="button"
|
||||
buttonType="contrast"
|
||||
bitIconButton="bwi-ellipsis-h"
|
||||
[label]="'switchAccount' | i18n"
|
||||
(click)="toggle()"
|
||||
[disabled]="disabled"
|
||||
aria-haspopup="dialog"
|
||||
></button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<ng-template
|
||||
cdkConnectedOverlay
|
||||
[cdkConnectedOverlayOrigin]="trigger"
|
||||
[cdkConnectedOverlayHasBackdrop]="true"
|
||||
[cdkConnectedOverlayBackdropClass]="'cdk-overlay-transparent-backdrop'"
|
||||
(backdropClick)="close()"
|
||||
(detach)="close()"
|
||||
[cdkConnectedOverlayOpen]="view.showSwitcher && isOpen"
|
||||
[cdkConnectedOverlayPositions]="overlayPosition"
|
||||
cdkConnectedOverlayMinWidth="250px"
|
||||
>
|
||||
<div
|
||||
class="account-switcher-dropdown"
|
||||
[@transformPanel]="'open'"
|
||||
cdkTrapFocus
|
||||
cdkTrapFocusAutoCapture
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div class="accounts" *ngIf="view.numberOfAccounts > 0">
|
||||
<button
|
||||
type="button"
|
||||
*ngFor="let account of view.inactiveAccounts | keyvalue"
|
||||
class="account account-v3"
|
||||
(click)="switch(account.key)"
|
||||
>
|
||||
<div class="tw-flex tw-gap-4 tw-items-center">
|
||||
@if (account.value.email != null) {
|
||||
<bit-avatar
|
||||
[text]="account.value.name ?? account.value.email"
|
||||
[id]="account.value.id"
|
||||
[color]="account.value.avatarColor"
|
||||
aria-hidden="true"
|
||||
></bit-avatar>
|
||||
}
|
||||
<div class="accountInfo">
|
||||
<span class="sr-only">{{ "switchAccount" | i18n }}: </span>
|
||||
<span class="email">{{ account.value.email }}</span>
|
||||
<span class="server">
|
||||
<span class="sr-only"> / </span>{{ account.value.server }}
|
||||
</span>
|
||||
<span class="status">
|
||||
<span class="sr-only"> (</span
|
||||
>{{
|
||||
(account.value.authenticationStatus === authStatus.Unlocked
|
||||
? "unlocked"
|
||||
: "locked"
|
||||
) | i18n
|
||||
}}<span class="sr-only">)</span></span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<i
|
||||
class="bwi bwi-2x text-muted"
|
||||
[ngClass]="
|
||||
account.value.authenticationStatus === authStatus.Unlocked ? 'bwi-unlock' : 'bwi-lock'
|
||||
"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
<ng-container *ngIf="view.activeAccount">
|
||||
<div class="border" *ngIf="view.numberOfAccounts > 0"></div>
|
||||
<ng-container *ngIf="view.numberOfAccounts < 4">
|
||||
<button type="button" class="add" (click)="addAccount()">
|
||||
<i class="bwi bwi-plus" aria-hidden="true"></i> {{ "addAccount" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="view.numberOfAccounts === 4">
|
||||
<span class="accountLimitReached">{{ "accountSwitcherLimitReached" | i18n }} </span>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,246 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { animate, state, style, transition, trigger } from "@angular/animations";
|
||||
import { A11yModule } from "@angular/cdk/a11y";
|
||||
import { OverlayModule, ConnectedPosition } from "@angular/cdk/overlay";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { toSignal } from "@angular/core/rxjs-interop";
|
||||
import { Router } from "@angular/router";
|
||||
import { combineLatest, firstValueFrom, map, Observable, switchMap } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { CommandDefinition, MessageListener } from "@bitwarden/common/platform/messaging";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { AvatarModule, IconButtonModule } from "@bitwarden/components";
|
||||
|
||||
import { DesktopBiometricsService } from "../../../key-management/biometrics/desktop.biometrics.service";
|
||||
|
||||
type ActiveAccount = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
avatarColor: string;
|
||||
server: string;
|
||||
};
|
||||
|
||||
type InactiveAccount = ActiveAccount & {
|
||||
authenticationStatus: AuthenticationStatus;
|
||||
};
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-account-switcher-v2",
|
||||
templateUrl: "account-switcher-v2.component.html",
|
||||
imports: [CommonModule, OverlayModule, A11yModule, JslibModule, AvatarModule, IconButtonModule],
|
||||
animations: [
|
||||
trigger("transformPanel", [
|
||||
state(
|
||||
"void",
|
||||
style({
|
||||
opacity: 0,
|
||||
}),
|
||||
),
|
||||
transition(
|
||||
"void => open",
|
||||
animate(
|
||||
"100ms linear",
|
||||
style({
|
||||
opacity: 1,
|
||||
}),
|
||||
),
|
||||
),
|
||||
transition("* => void", animate("100ms linear", style({ opacity: 0 }))),
|
||||
]),
|
||||
],
|
||||
})
|
||||
export class AccountSwitcherV2Component implements OnInit {
|
||||
protected readonly useNewAccountSwitcher = toSignal(
|
||||
this.configService.getFeatureFlag$(FeatureFlag.DesktopUiMigrationMilestone4),
|
||||
{ initialValue: false },
|
||||
);
|
||||
|
||||
activeAccount$: Observable<ActiveAccount | null>;
|
||||
inactiveAccounts$: Observable<{ [userId: string]: InactiveAccount }>;
|
||||
authStatus = AuthenticationStatus;
|
||||
|
||||
view$: Observable<{
|
||||
activeAccount: ActiveAccount | null;
|
||||
inactiveAccounts: { [userId: string]: InactiveAccount };
|
||||
numberOfAccounts: number;
|
||||
showSwitcher: boolean;
|
||||
}>;
|
||||
|
||||
isOpen = false;
|
||||
overlayPosition: ConnectedPosition[] = [
|
||||
{
|
||||
originX: "end",
|
||||
originY: "bottom",
|
||||
overlayX: "end",
|
||||
overlayY: "top",
|
||||
},
|
||||
];
|
||||
|
||||
showSwitcher$: Observable<boolean>;
|
||||
|
||||
numberOfAccounts$: Observable<number>;
|
||||
disabled = false;
|
||||
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private avatarService: AvatarService,
|
||||
private messagingService: MessagingService,
|
||||
private messageListener: MessageListener,
|
||||
private router: Router,
|
||||
private environmentService: EnvironmentService,
|
||||
private accountService: AccountService,
|
||||
private biometricsService: DesktopBiometricsService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.activeAccount$ = this.accountService.activeAccount$.pipe(
|
||||
switchMap(async (active) => {
|
||||
if (active == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!active.name && !active.email) {
|
||||
// We need to have this information at minimum to display them.
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: active.id,
|
||||
name: active.name,
|
||||
email: active.email,
|
||||
avatarColor: await firstValueFrom(this.avatarService.avatarColor$),
|
||||
server: (
|
||||
await firstValueFrom(this.environmentService.getEnvironment$(active.id))
|
||||
)?.getHostname(),
|
||||
};
|
||||
}),
|
||||
);
|
||||
this.inactiveAccounts$ = combineLatest([
|
||||
this.activeAccount$,
|
||||
this.accountService.accounts$,
|
||||
this.authService.authStatuses$,
|
||||
]).pipe(
|
||||
switchMap(async ([activeAccount, accounts, accountStatuses]) => {
|
||||
// Filter out logged out accounts and active account
|
||||
accounts = Object.fromEntries(
|
||||
Object.entries(accounts).filter(
|
||||
([id]: [UserId, AccountInfo]) =>
|
||||
accountStatuses[id] !== AuthenticationStatus.LoggedOut || id === activeAccount?.id,
|
||||
),
|
||||
);
|
||||
return this.createInactiveAccounts(accounts);
|
||||
}),
|
||||
);
|
||||
this.showSwitcher$ = combineLatest([this.activeAccount$, this.inactiveAccounts$]).pipe(
|
||||
map(([activeAccount, inactiveAccounts]) => {
|
||||
const hasActiveUser = activeAccount != null;
|
||||
const userIsAddingAnAdditionalAccount = Object.keys(inactiveAccounts).length > 0;
|
||||
return hasActiveUser || userIsAddingAnAdditionalAccount;
|
||||
}),
|
||||
);
|
||||
this.numberOfAccounts$ = this.inactiveAccounts$.pipe(
|
||||
map((accounts) => Object.keys(accounts).length),
|
||||
);
|
||||
|
||||
this.view$ = combineLatest([
|
||||
this.activeAccount$,
|
||||
this.inactiveAccounts$,
|
||||
this.numberOfAccounts$,
|
||||
this.showSwitcher$,
|
||||
]).pipe(
|
||||
map(([activeAccount, inactiveAccounts, numberOfAccounts, showSwitcher]) => ({
|
||||
activeAccount,
|
||||
inactiveAccounts,
|
||||
numberOfAccounts,
|
||||
showSwitcher,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
const active = await firstValueFrom(this.accountService.activeAccount$);
|
||||
if (active == null) {
|
||||
return;
|
||||
}
|
||||
const authStatus = await firstValueFrom(
|
||||
this.authService.authStatuses$.pipe(map((statuses) => statuses[active.id])),
|
||||
);
|
||||
if (authStatus === AuthenticationStatus.LoggedOut) {
|
||||
const nextUpAccount = await firstValueFrom(this.accountService.nextUpAccount$);
|
||||
if (nextUpAccount != null) {
|
||||
await this.switch(nextUpAccount.id);
|
||||
} else {
|
||||
await this.addAccount();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.isOpen = !this.isOpen;
|
||||
}
|
||||
|
||||
close() {
|
||||
this.isOpen = false;
|
||||
}
|
||||
|
||||
async switch(userId: string) {
|
||||
this.close();
|
||||
await this.biometricsService.setShouldAutopromptNow(true);
|
||||
|
||||
this.disabled = true;
|
||||
const accountSwitchFinishedPromise = firstValueFrom(
|
||||
this.messageListener.messages$(new CommandDefinition("finishSwitchAccount")),
|
||||
);
|
||||
this.messagingService.send("switchAccount", { userId });
|
||||
await accountSwitchFinishedPromise;
|
||||
this.disabled = false;
|
||||
}
|
||||
|
||||
async addAccount() {
|
||||
this.close();
|
||||
|
||||
await this.accountService.switchAccount(null);
|
||||
await this.router.navigate(["/login"]);
|
||||
}
|
||||
|
||||
private async createInactiveAccounts(baseAccounts: {
|
||||
[userId: string]: AccountInfo;
|
||||
}): Promise<{ [userId: string]: InactiveAccount }> {
|
||||
const inactiveAccounts: { [userId: string]: InactiveAccount } = {};
|
||||
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
for (const userId in baseAccounts) {
|
||||
if (userId == null || userId === activeUserId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
inactiveAccounts[userId] = {
|
||||
id: userId,
|
||||
name: baseAccounts[userId].name,
|
||||
email: baseAccounts[userId].email,
|
||||
authenticationStatus: await this.authService.getAuthStatus(userId),
|
||||
avatarColor: await firstValueFrom(this.avatarService.getUserAvatarColor$(userId as UserId)),
|
||||
server: (
|
||||
await firstValueFrom(this.environmentService.getEnvironment$(userId as UserId))
|
||||
)?.getHostname(),
|
||||
};
|
||||
}
|
||||
|
||||
return inactiveAccounts;
|
||||
}
|
||||
}
|
||||
@@ -145,6 +145,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.account-switcher-v2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.account-switcher-dropdown {
|
||||
@include themify($themes) {
|
||||
background-color: themed("accountSwitcherBackgroundColor");
|
||||
@@ -206,6 +216,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.account-v3 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.border {
|
||||
|
||||
@@ -88,6 +88,7 @@ export enum FeatureFlag {
|
||||
DesktopUiMigrationMilestone1 = "desktop-ui-migration-milestone-1",
|
||||
DesktopUiMigrationMilestone2 = "desktop-ui-migration-milestone-2",
|
||||
DesktopUiMigrationMilestone3 = "desktop-ui-migration-milestone-3",
|
||||
DesktopUiMigrationMilestone4 = "desktop-ui-migration-milestone-4",
|
||||
|
||||
/* UIF */
|
||||
RouterFocusManagement = "router-focus-management",
|
||||
@@ -185,6 +186,7 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.DesktopUiMigrationMilestone1]: FALSE,
|
||||
[FeatureFlag.DesktopUiMigrationMilestone2]: FALSE,
|
||||
[FeatureFlag.DesktopUiMigrationMilestone3]: FALSE,
|
||||
[FeatureFlag.DesktopUiMigrationMilestone4]: FALSE,
|
||||
|
||||
/* UIF */
|
||||
[FeatureFlag.RouterFocusManagement]: FALSE,
|
||||
|
||||
Reference in New Issue
Block a user