1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-12 06:23:38 +00:00

Merge remote-tracking branch 'origin/main' into extension-signing

This commit is contained in:
Michal Checinski
2024-09-16 13:45:18 +02:00
23 changed files with 438 additions and 57 deletions

View File

@@ -179,6 +179,18 @@
"addItem": {
"message": "Add item"
},
"accountEmail": {
"message": "Account email"
},
"requestHint": {
"message": "Request hint"
},
"requestPasswordHint": {
"message": "Request password hint"
},
"enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": {
"message": "Enter your account email address and your password hint will be sent to you"
},
"passwordHint": {
"message": "Password hint"
},

View File

@@ -1,6 +1,8 @@
import { Injectable, NgModule } from "@angular/core";
import { ActivatedRouteSnapshot, RouteReuseStrategy, RouterModule, Routes } from "@angular/router";
import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component";
import { unauthUiRefreshSwap } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-route-swap";
import {
authGuard,
lockGuard,
@@ -15,11 +17,13 @@ import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh
import {
AnonLayoutWrapperComponent,
AnonLayoutWrapperData,
PasswordHintComponent,
RegistrationFinishComponent,
RegistrationStartComponent,
RegistrationStartSecondaryComponent,
RegistrationStartSecondaryComponentData,
SetPasswordJitComponent,
UserLockIcon,
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
@@ -27,6 +31,7 @@ import { twofactorRefactorSwap } from "../../../../libs/angular/src/utils/two-fa
import { fido2AuthGuard } from "../auth/guards/fido2-auth.guard";
import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component";
import { EnvironmentComponent } from "../auth/popup/environment.component";
import { ExtensionAnonLayoutWrapperComponent } from "../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component";
import { HintComponent } from "../auth/popup/hint.component";
import { HomeComponent } from "../auth/popup/home.component";
import { LockComponent } from "../auth/popup/lock.component";
@@ -213,12 +218,6 @@ const routes: Routes = [
canActivate: [unauthGuardFn(unauthRouteOverrides)],
data: { state: "register" },
},
{
path: "hint",
component: HintComponent,
canActivate: [unauthGuardFn(unauthRouteOverrides)],
data: { state: "hint" },
},
{
path: "environment",
component: EnvironmentComponent,
@@ -385,6 +384,41 @@ const routes: Routes = [
canActivate: [authGuard],
data: { state: "update-temp-password" },
},
...unauthUiRefreshSwap(
HintComponent,
ExtensionAnonLayoutWrapperComponent,
{
path: "hint",
canActivate: [unauthGuardFn(unauthRouteOverrides)],
data: {
state: "hint",
},
},
{
path: "",
children: [
{
path: "hint",
canActivate: [unauthGuardFn(unauthRouteOverrides)],
data: {
pageTitle: "requestPasswordHint",
pageSubtitle: "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou",
pageIcon: UserLockIcon,
showBackButton: true,
state: "hint",
},
children: [
{ path: "", component: PasswordHintComponent },
{
path: "",
component: EnvironmentSelectorComponent,
outlet: "environment-selector",
},
],
},
],
},
),
{
path: "",
component: AnonLayoutWrapperComponent,

View File

@@ -20,6 +20,7 @@ import { AvatarModule, ButtonModule, ToastModule } from "@bitwarden/components";
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";
import { ExtensionAnonLayoutWrapperComponent } from "../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component";
import { HintComponent } from "../auth/popup/hint.component";
import { HomeComponent } from "../auth/popup/home.component";
import { LockComponent } from "../auth/popup/lock.component";
@@ -131,6 +132,7 @@ import "../platform/popup/locales";
HeaderComponent,
UserVerificationDialogComponent,
CurrentAccountComponent,
ExtensionAnonLayoutWrapperComponent,
],
declarations: [
ActionButtonsComponent,

View File

@@ -1,6 +1,8 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component";
import { unauthUiRefreshSwap } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-route-swap";
import {
authGuard,
lockGuard,
@@ -12,11 +14,13 @@ import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag
import {
AnonLayoutWrapperComponent,
AnonLayoutWrapperData,
PasswordHintComponent,
RegistrationFinishComponent,
RegistrationStartComponent,
RegistrationStartSecondaryComponent,
RegistrationStartSecondaryComponentData,
SetPasswordJitComponent,
UserLockIcon,
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
@@ -94,7 +98,6 @@ const routes: Routes = [
canActivate: [authGuard],
},
{ path: "accessibility-cookie", component: AccessibilityCookieComponent },
{ path: "hint", component: HintComponent },
{ path: "set-password", component: SetPasswordComponent },
{ path: "sso", component: SsoComponent },
{
@@ -113,6 +116,41 @@ const routes: Routes = [
canActivate: [authGuard],
data: { titleId: "removeMasterPassword" },
},
...unauthUiRefreshSwap(
HintComponent,
AnonLayoutWrapperComponent,
{
path: "hint",
canActivate: [unauthGuardFn()],
data: {
pageTitle: "passwordHint",
titleId: "passwordHint",
},
},
{
path: "",
children: [
{
path: "hint",
canActivate: [unauthGuardFn()],
data: {
pageTitle: "requestPasswordHint",
pageSubtitle: "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou",
pageIcon: UserLockIcon,
state: "hint",
},
children: [
{ path: "", component: PasswordHintComponent },
{
path: "",
component: EnvironmentSelectorComponent,
outlet: "environment-selector",
},
],
},
],
},
),
{
path: "",
component: AnonLayoutWrapperComponent,

View File

@@ -194,9 +194,9 @@ export class AppComponent implements OnInit, OnDestroy {
break;
case "loggedOut":
this.modalService.closeAll();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.notificationsService.updateConnection();
if (message.userId == null || message.userId === this.activeUserId) {
await this.notificationsService.updateConnection();
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.updateAppMenu();
@@ -694,9 +694,7 @@ export class AppComponent implements OnInit, OnDestroy {
// This must come last otherwise the logout will prematurely trigger
// a process reload before all the state service user data can be cleaned up
if (userBeingLoggedOut === activeUserId) {
this.authService.logOut(async () => {});
}
this.authService.logOut(async () => {}, userBeingLoggedOut);
}
private async recordActivity() {

View File

@@ -560,6 +560,18 @@
"settings": {
"message": "Settings"
},
"accountEmail": {
"message": "Account email"
},
"requestHint": {
"message": "Request hint"
},
"requestPasswordHint": {
"message": "Request password hint"
},
"enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": {
"message": "Enter your account email address and your password hint will be sent to you"
},
"passwordHint": {
"message": "Password hint"
},

View File

@@ -119,9 +119,12 @@ export class AppComponent implements OnDestroy, OnInit {
this.notificationsService.updateConnection(false);
break;
case "loggedOut":
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.notificationsService.updateConnection(false);
if (
message.userId == null ||
message.userId === (await firstValueFrom(this.accountService.activeAccount$))
) {
await this.notificationsService.updateConnection(false);
}
break;
case "unlocked":
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
@@ -311,7 +314,7 @@ export class AppComponent implements OnDestroy, OnInit {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/"]);
}
});
}, userId);
}
private async recordActivity() {

View File

@@ -53,10 +53,19 @@
[class]="'tw-grid-cols-' + selectableProducts.length"
>
<div
*ngFor="let selectableProduct of selectableProducts; let i = index"
*ngFor="
let selectableProduct of selectableProducts;
trackBy: manageSelectableProduct;
let i = index
"
[ngClass]="getPlanCardContainerClasses(selectableProduct, i)"
(click)="selectPlan(selectableProduct)"
tabindex="0"
[attr.tabindex]="focusedIndex !== i || isCardDisabled(i) ? '-1' : '0'"
class="product-card"
(keyup)="onKeydown($event, i)"
(focus)="onFocus(i)"
[attr.aria-disabled]="isCardDisabled(i)"
[id]="i + 'a_plan_card'"
>
<div class="tw-relative">
<div

View File

@@ -160,6 +160,9 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
showPayment: boolean = false;
totalOpened: boolean = false;
currentPlan: PlanResponse;
currentFocusIndex = 0;
isCardStateDisabled = false;
focusedIndex: number | null = null;
deprecateStripeSourcesAPI: boolean;
@@ -255,6 +258,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
}
setInitialPlanSelection() {
this.focusedIndex = this.selectableProducts.length - 1;
this.selectPlan(this.getPlanByType(ProductTierType.Enterprise));
}
@@ -307,15 +311,22 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
if (plan == this.currentPlan) {
cardState = PlanCardState.Disabled;
this.isCardStateDisabled = true;
this.focusedIndex = index;
} else if (plan == this.selectedPlan) {
cardState = PlanCardState.Selected;
this.isCardStateDisabled = false;
this.focusedIndex = index;
} else if (
this.selectedInterval === PlanInterval.Monthly &&
plan.productTier == ProductTierType.Families
) {
cardState = PlanCardState.Disabled;
this.isCardStateDisabled = true;
this.focusedIndex = this.selectableProducts.length - 1;
} else {
cardState = PlanCardState.NotSelected;
this.isCardStateDisabled = false;
}
switch (cardState) {
@@ -466,7 +477,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
}
get storageGb() {
return this.sub?.maxStorageGb - 1;
return this.sub?.maxStorageGb ? this.sub?.maxStorageGb - 1 : 0;
}
passwordManagerSeatTotal(plan: PlanResponse): number {
@@ -492,7 +503,8 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
}
return (
plan.PasswordManager.additionalStoragePricePerGb * Math.abs(this.sub?.maxStorageGb - 1 || 0)
plan.PasswordManager.additionalStoragePricePerGb *
Math.abs(this.sub?.maxStorageGb ? this.sub?.maxStorageGb - 1 : 0 || 0)
);
}
@@ -861,4 +873,44 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
return this.i18nService.t("planNameTeams");
}
}
onKeydown(event: KeyboardEvent, index: number) {
const cardElements = Array.from(document.querySelectorAll(".product-card")) as HTMLElement[];
let newIndex = index;
const direction = event.key === "ArrowRight" || event.key === "ArrowDown" ? 1 : -1;
if (["ArrowRight", "ArrowDown", "ArrowLeft", "ArrowUp"].includes(event.key)) {
do {
newIndex = (newIndex + direction + cardElements.length) % cardElements.length;
} while (this.isCardDisabled(newIndex) && newIndex !== index);
event.preventDefault();
setTimeout(() => {
const card = cardElements[newIndex];
if (
!(
card.classList.contains("tw-bg-secondary-100") &&
card.classList.contains("tw-text-muted")
)
) {
card?.focus();
}
}, 0);
}
}
onFocus(index: number) {
this.focusedIndex = index;
this.selectPlan(this.selectableProducts[index]);
}
isCardDisabled(index: number): boolean {
const card = this.selectableProducts[index];
return card === (this.currentPlan || this.isCardStateDisabled);
}
manageSelectableProduct(index: number) {
return index;
}
}

View File

@@ -1,6 +1,7 @@
import { NgModule } from "@angular/core";
import { Route, RouterModule, Routes } from "@angular/router";
import { unauthUiRefreshSwap } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-route-swap";
import {
authGuard,
lockGuard,
@@ -12,13 +13,15 @@ import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag
import {
AnonLayoutWrapperComponent,
AnonLayoutWrapperData,
PasswordHintComponent,
RegistrationFinishComponent,
RegistrationStartComponent,
RegistrationStartSecondaryComponent,
RegistrationStartSecondaryComponentData,
SetPasswordJitComponent,
LockIcon,
RegistrationLinkExpiredComponent,
LockIcon,
UserLockIcon,
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
@@ -167,6 +170,49 @@ const routes: Routes = [
},
],
},
...unauthUiRefreshSwap(
AnonLayoutWrapperComponent,
AnonLayoutWrapperComponent,
{
path: "hint",
canActivate: [unauthGuardFn()],
data: {
pageTitle: "passwordHint",
titleId: "passwordHint",
},
children: [
{ path: "", component: HintComponent },
{
path: "",
component: EnvironmentSelectorComponent,
outlet: "environment-selector",
},
],
},
{
path: "",
children: [
{
path: "hint",
canActivate: [unauthGuardFn()],
data: {
pageTitle: "requestPasswordHint",
pageSubtitle: "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou",
pageIcon: UserLockIcon,
state: "hint",
},
children: [
{ path: "", component: PasswordHintComponent },
{
path: "",
component: EnvironmentSelectorComponent,
outlet: "environment-selector",
},
],
},
],
},
),
{
path: "",
component: AnonLayoutWrapperComponent,
@@ -388,25 +434,6 @@ const routes: Routes = [
},
],
},
{
path: "hint",
canActivate: [unauthGuardFn()],
data: {
pageTitle: "passwordHint",
titleId: "passwordHint",
} satisfies DataProperties & AnonLayoutWrapperData,
children: [
{
path: "",
component: HintComponent,
},
{
path: "",
component: EnvironmentSelectorComponent,
outlet: "environment-selector",
},
],
},
{
path: "remove-password",
component: RemovePasswordComponent,

View File

@@ -960,6 +960,18 @@
"settings": {
"message": "Settings"
},
"accountEmail": {
"message": "Account email"
},
"requestHint": {
"message": "Request hint"
},
"requestPasswordHint": {
"message": "Request password hint"
},
"enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": {
"message": "Enter your account email address and your password hint will be sent to you"
},
"passwordHint": {
"message": "Password hint"
},
@@ -9050,7 +9062,7 @@
"message": "Directory integration"
},
"passwordLessSso": {
"message": "PasswordLess SSO"
"message": "Passwordless SSO"
},
"accountRecovery": {
"message": "Account recovery"

View File

@@ -317,6 +317,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
// Try to load from memory first
const email = await firstValueFrom(this.loginEmailService.loginEmail$);
const rememberEmail = this.loginEmailService.getRememberEmail();
if (email) {
this.formGroup.controls.email.setValue(email);
this.formGroup.controls.rememberEmail.setValue(rememberEmail);

View File

@@ -20,6 +20,7 @@ export function unauthUiRefreshSwap(
defaultComponent: Type<any>,
refreshedComponent: Type<any>,
options: Route,
altOptions?: Route,
): Routes {
return componentRouteSwap(
defaultComponent,
@@ -29,5 +30,6 @@ export function unauthUiRefreshSwap(
return configService.getFeatureFlag(FeatureFlag.UnauthenticatedExtensionUIRefresh);
},
options,
altOptions,
);
}

View File

@@ -1,9 +1,12 @@
<!-- The calc() reductions are to account for browser/desktop headers -->
<main
class="tw-flex tw-min-h-screen tw-w-full tw-mx-auto tw-flex-col tw-gap-7 tw-bg-background-alt tw-px-8 tw-pb-4 tw-text-main"
class="tw-flex tw-w-full tw-mx-auto tw-flex-col tw-gap-7 tw-bg-background-alt tw-px-8 tw-pb-4 tw-text-main"
[ngClass]="{
'tw-pt-0': decreaseTopPadding,
'tw-pt-8': !decreaseTopPadding,
'tw-relative tw-top-0': clientType === 'browser',
'tw-min-h-screen': clientType === 'web',
'tw-min-h-[calc(100vh-72px)]': clientType === 'browser',
'tw-min-h-[calc(100vh-54px)]': clientType === 'desktop',
}"
>
<bit-icon *ngIf="!hideLogo" [icon]="logo" class="tw-w-[128px] [&>*]:tw-align-top"></bit-icon>
@@ -23,6 +26,7 @@
{{ title }}
</h1>
</ng-container>
<div *ngIf="subtitle" class="tw-text-sm sm:tw-text-base">{{ subtitle }}</div>
</div>

View File

@@ -1,4 +1,5 @@
export * from "./bitwarden-logo.icon";
export * from "./bitwarden-shield.icon";
export * from "./lock.icon";
export * from "./user-lock.icon";
export * from "./user-verification-biometrics-fingerprint.icon";

View File

@@ -0,0 +1,22 @@
import { svgIcon } from "@bitwarden/components";
export const UserLockIcon = svgIcon`
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="100" fill="none">
<path class="tw-fill-text-headers" fill-rule="evenodd" d="M0 18.207a7.798 7.798 0 0 1 7.798-7.798H89.38a7.798 7.798 0 0 1 7.799 7.798v8.763h-2.4v-8.763a5.399 5.399 0 0 0-5.398-5.399H7.797A5.399 5.399 0 0 0 2.4 18.207v49.19a5.399 5.399 0 0 0 5.4 5.398h9.483v2.4H7.798A7.798 7.798 0 0 1 0 67.396V18.207Zm49.378 54.588h13.498v2.4H49.378v-2.4Z" clip-rule="evenodd"/>
<path class="tw-fill-text-headers" fill-rule="evenodd" d="M88.78 58.398c8.946 0 16.197-7.251 16.197-16.196s-7.251-16.197-16.196-16.197-16.197 7.252-16.197 16.197 7.252 16.196 16.197 16.196Zm0 2.4c10.27 0 18.597-8.326 18.597-18.596s-8.326-18.596-18.596-18.596-18.596 8.326-18.596 18.596S78.51 60.798 88.78 60.798Z" clip-rule="evenodd"/>
<path class="tw-fill-text-headers" fill-rule="evenodd" d="M61.005 87.192h54.28c1.303 0 1.833-.344 2.01-.53.128-.134.396-.517.226-1.586-2.187-13.74-14.213-24.278-28.752-24.278S62.203 71.337 60.017 85.076c-.09.57.026 1.226.279 1.662.115.2.23.305.316.359.072.046.184.095.393.095Zm0 2.4h54.28c3.346 0 5.104-1.76 4.605-4.893-2.371-14.903-15.402-26.3-31.121-26.3-15.72 0-28.75 11.397-31.122 26.3-.337 2.121.744 4.893 3.358 4.893Z" clip-rule="evenodd"/>
<path class="tw-fill-info-600" fill-rule="evenodd" d="M77.983 17.607a1.2 1.2 0 0 1-1.2-1.2v-.6a1.2 1.2 0 1 1 2.4 0v.6a1.2 1.2 0 0 1-1.2 1.2ZM83.382 17.607a1.2 1.2 0 0 1-1.2-1.2v-.498a1.2 1.2 0 1 1 2.4 0v.498a1.2 1.2 0 0 1-1.2 1.2ZM88.78 17.607a1.2 1.2 0 0 1-1.2-1.2v-.498a1.2 1.2 0 1 1 2.4 0v.498a1.2 1.2 0 0 1-1.2 1.2Z" clip-rule="evenodd"/>
<path class="tw-fill-text-headers" fill-rule="evenodd" d="M95.083 20.607H0v-1.2h95.083v1.2ZM15.12 54.571a5.999 5.999 0 0 1 6-5.998h23.23a5.999 5.999 0 0 1 6 5.998V57.2h-2.4V54.57a3.6 3.6 0 0 0-3.6-3.599H21.12a3.6 3.6 0 0 0-3.6 3.6v2.627h-2.4V54.57Zm2.4 15.825v2.693a3.6 3.6 0 0 0 3.6 3.599h23.23a3.6 3.6 0 0 0 3.6-3.6v-2.692h2.4v2.693a5.999 5.999 0 0 1-6 5.998H21.12a5.999 5.999 0 0 1-6-5.998v-2.693h2.4Z" clip-rule="evenodd"/>
<path class="tw-fill-text-headers" fill-rule="evenodd" d="M32.479 33.255c-5.31 0-9.641 4.332-9.641 9.64v6.822h-2.4v-6.821c0-6.635 5.406-12.04 12.04-12.04 6.633 0 12.041 5.377 12.041 12.04v6.821h-2.4v-6.821c0-5.334-4.33-9.641-9.64-9.641Z" clip-rule="evenodd"/>
<path class="tw-fill-info-600" fill-rule="evenodd" d="M10.498 63.797a7.498 7.498 0 0 1 7.498-7.498h30.593a7.498 7.498 0 0 1 0 14.997H17.996a7.498 7.498 0 0 1-7.498-7.499Zm43.79 0a5.699 5.699 0 0 0-5.699-5.699H17.996a5.699 5.699 0 0 0 0 11.398h30.593a5.699 5.699 0 0 0 5.7-5.699Z" clip-rule="evenodd"/>
<path class="tw-fill-info-600" fill-rule="evenodd" d="M19.02 60.669a.6.6 0 0 1 .6.6v2.959a.6.6 0 0 1-1.2 0v-2.96a.6.6 0 0 1 .6-.6Z" clip-rule="evenodd"/>
<path class="tw-fill-info-600" fill-rule="evenodd" d="M22.443 63.13a.6.6 0 0 1-.388.755l-2.851.914a.6.6 0 1 1-.367-1.142l2.852-.915a.6.6 0 0 1 .754.388Z" clip-rule="evenodd"/>
<path class="tw-fill-info-600" fill-rule="evenodd" d="M18.67 63.742a.6.6 0 0 1 .837.135l1.748 2.42a.6.6 0 0 1-.972.703l-1.749-2.42a.6.6 0 0 1 .136-.838Z" clip-rule="evenodd"/>
<path class="tw-fill-info-600" fill-rule="evenodd" d="M19.368 63.739a.6.6 0 0 1 .142.837l-1.722 2.42a.6.6 0 0 1-.978-.695l1.722-2.42a.6.6 0 0 1 .836-.142Z" clip-rule="evenodd"/>
<path class="tw-fill-info-600" fill-rule="evenodd" d="M15.626 63.129a.6.6 0 0 1 .755-.386l2.825.914a.6.6 0 0 1-.37 1.142l-2.824-.915a.6.6 0 0 1-.386-.755ZM28.651 60.669a.6.6 0 0 1 .6.6v2.959a.6.6 0 1 1-1.2 0v-2.96a.6.6 0 0 1 .6-.6Z" clip-rule="evenodd"/>
<path class="tw-fill-info-600" fill-rule="evenodd" d="M32.046 63.129a.6.6 0 0 1-.386.755l-2.824.915a.6.6 0 1 1-.37-1.142l2.825-.914a.6.6 0 0 1 .755.386Z" clip-rule="evenodd"/>
<path class="tw-fill-info-600" fill-rule="evenodd" d="M28.3 63.742a.6.6 0 0 1 .837.135l1.749 2.42a.6.6 0 0 1-.973.703l-1.748-2.42a.6.6 0 0 1 .135-.838Z" clip-rule="evenodd"/>
<path class="tw-fill-info-600" fill-rule="evenodd" d="M29.002 63.742a.6.6 0 0 1 .136.837L27.387 67a.6.6 0 0 1-.972-.702l1.749-2.421a.6.6 0 0 1 .837-.136Z" clip-rule="evenodd"/>
<path class="tw-fill-info-600" fill-rule="evenodd" d="M25.256 63.129a.6.6 0 0 1 .755-.386l2.825.914a.6.6 0 1 1-.37 1.142l-2.824-.915a.6.6 0 0 1-.386-.755ZM34.857 66.649a.6.6 0 0 1 .6-.6h5.649a.6.6 0 0 1 0 1.2h-5.649a.6.6 0 0 1-.6-.6ZM44.487 66.649a.6.6 0 0 1 .6-.6h5.65a.6.6 0 0 1 0 1.2h-5.65a.6.6 0 0 1-.6-.6Z" clip-rule="evenodd"/>
</svg>
`;

View File

@@ -16,7 +16,9 @@ export * from "./fingerprint-dialog/fingerprint-dialog.component";
// password callout
export * from "./password-callout/password-callout.component";
export * from "./vault-timeout-input/vault-timeout-input.component";
// password hint
export * from "./password-hint/password-hint.component";
// input password
export * from "./input-password/input-password.component";
@@ -40,3 +42,6 @@ export * from "./registration/registration-start/registration-start-secondary.co
export * from "./registration/registration-env-selector/registration-env-selector.component";
export * from "./registration/registration-finish/registration-finish.service";
export * from "./registration/registration-finish/default-registration-finish.service";
// vault timeout
export * from "./vault-timeout-input/vault-timeout-input.component";

View File

@@ -0,0 +1,40 @@
<form [bitSubmit]="submit" [formGroup]="formGroup">
<!-- <ng-template> must be within the <form> to ensure that `formControlName` has a parent `formGroup` directive. -->
<ng-template #formContentTemplate>
<bit-form-field>
<bit-label>{{ "accountEmail" | i18n }}</bit-label>
<input
bitInput
appAutofocus
inputmode="email"
appInputVerbatim="false"
type="email"
formControlName="email"
/>
</bit-form-field>
<button
class="tw-mb-2"
type="submit"
bitButton
bitFormButton
buttonType="primary"
[block]="true"
>
{{ "requestHint" | i18n }}
</button>
<button type="button" bitButton buttonType="secondary" (click)="cancel()" [block]="true">
{{ "cancel" | i18n }}
</button>
</ng-template>
<!-- Browser -->
<main *ngIf="clientType === 'browser'" tabindex="-1">
<ng-container *ngTemplateOutlet="formContentTemplate"></ng-container>
</main>
<!-- Web, Desktop -->
<ng-container *ngIf="clientType !== 'browser'">
<ng-container *ngTemplateOutlet="formContentTemplate"></ng-container>
</ng-container>
</form>

View File

@@ -0,0 +1,107 @@
import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
import { Router, RouterModule } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PasswordHintRequest } from "@bitwarden/common/auth/models/request/password-hint.request";
import { ClientType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import {
AsyncActionsModule,
ButtonModule,
FormFieldModule,
ToastService,
} from "@bitwarden/components";
@Component({
standalone: true,
templateUrl: "./password-hint.component.html",
imports: [
AsyncActionsModule,
ButtonModule,
CommonModule,
FormFieldModule,
JslibModule,
ReactiveFormsModule,
RouterModule,
],
})
export class PasswordHintComponent implements OnInit {
protected clientType: ClientType;
protected formGroup = this.formBuilder.group({
email: ["", [Validators.required, Validators.email]],
});
protected get email() {
return this.formGroup.controls.email.value;
}
constructor(
private apiService: ApiService,
private formBuilder: FormBuilder,
private i18nService: I18nService,
private loginEmailService: LoginEmailServiceAbstraction,
private platformUtilsService: PlatformUtilsService,
private toastService: ToastService,
private router: Router,
) {
this.clientType = this.platformUtilsService.getClientType();
}
async ngOnInit(): Promise<void> {
const email = (await firstValueFrom(this.loginEmailService.loginEmail$)) ?? "";
this.formGroup.controls.email.setValue(email);
}
submit = async () => {
const isEmailValid = this.validateEmailOrShowToast(this.email);
if (!isEmailValid) {
return;
}
await this.apiService.postPasswordHint(new PasswordHintRequest(this.email));
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("masterPassSent"),
});
await this.router.navigate(["login"]);
};
protected async cancel() {
this.loginEmailService.setLoginEmail(this.email);
await this.router.navigate(["login"]);
}
private validateEmailOrShowToast(email: string): boolean {
// If email is null or empty, show error toast and return false
if (email == null || email === "") {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("emailRequired"),
});
return false;
}
// If not a valid email format, show error toast and return false
if (email.indexOf("@") === -1) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("invalidEmail"),
});
return false;
}
return true; // email is valid
}
}

View File

@@ -16,5 +16,5 @@ export abstract class AuthService {
abstract authStatusFor$(userId: UserId): Observable<AuthenticationStatus>;
/** @deprecated use {@link activeAccountStatus$} instead */
abstract getAuthStatus: (userId?: string) => Promise<AuthenticationStatus>;
abstract logOut: (callback: () => void) => void;
abstract logOut: (callback: () => void, userId?: string) => void;
}

View File

@@ -93,8 +93,8 @@ export class AuthService implements AuthServiceAbstraction {
return await firstValueFrom(this.authStatusFor$(userId as UserId));
}
logOut(callback: () => void) {
logOut(callback: () => void, userId?: string): void {
callback();
this.messageSender.send("loggedOut");
this.messageSender.send("loggedOut", { userId });
}
}

16
package-lock.json generated
View File

@@ -119,7 +119,7 @@
"@typescript-eslint/eslint-plugin": "7.16.1",
"@typescript-eslint/parser": "7.16.1",
"@webcomponents/custom-elements": "1.6.0",
"@yao-pkg/pkg": "5.12.1",
"@yao-pkg/pkg": "5.14.0",
"autoprefixer": "10.4.20",
"babel-loader": "9.1.3",
"base64-loader": "1.0.0",
@@ -10828,16 +10828,16 @@
"license": "Apache-2.0"
},
"node_modules/@yao-pkg/pkg": {
"version": "5.12.1",
"resolved": "https://registry.npmjs.org/@yao-pkg/pkg/-/pkg-5.12.1.tgz",
"integrity": "sha512-vqp8Z9o39LDKTpjfeDjJsLf4mi0zS4jkbTTZbptfc/K1KKDU2hosex64TaattPO9NLkibc6EJldmSjVmc63ooA==",
"version": "5.14.0",
"resolved": "https://registry.npmjs.org/@yao-pkg/pkg/-/pkg-5.14.0.tgz",
"integrity": "sha512-34oflUyAOI64a4cc4AF3ckvS8Qqnk/ISvZ1bDBa1/JAYaaFtzAO+RlhPaU+wCHzhk6VXvZwEywJpb+SlVDTgdA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/generator": "7.23.0",
"@babel/parser": "7.23.0",
"@babel/types": "7.23.0",
"@yao-pkg/pkg-fetch": "3.5.9",
"@yao-pkg/pkg-fetch": "3.5.11",
"chalk": "^4.1.2",
"fs-extra": "^9.1.0",
"globby": "^11.1.0",
@@ -10854,9 +10854,9 @@
}
},
"node_modules/@yao-pkg/pkg-fetch": {
"version": "3.5.9",
"resolved": "https://registry.npmjs.org/@yao-pkg/pkg-fetch/-/pkg-fetch-3.5.9.tgz",
"integrity": "sha512-usMwwqFCd2B7k+V87u6kiTesyDSlw+3LpiuYBWe+UgryvSOk/NXjx3XVCub8hQoi0bCREbdQ6NDBqminyHJJrg==",
"version": "3.5.11",
"resolved": "https://registry.npmjs.org/@yao-pkg/pkg-fetch/-/pkg-fetch-3.5.11.tgz",
"integrity": "sha512-2tQ/1n7BLTptW6lL0pfTCnVMIxls8Jiw0/ClK1J2Fja9z2S2j4uzNL5dwGRqtvPJPn/q9i8X+Y+c4dwnMb+NOA==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@@ -81,7 +81,7 @@
"@typescript-eslint/eslint-plugin": "7.16.1",
"@typescript-eslint/parser": "7.16.1",
"@webcomponents/custom-elements": "1.6.0",
"@yao-pkg/pkg": "5.12.1",
"@yao-pkg/pkg": "5.14.0",
"autoprefixer": "10.4.20",
"babel-loader": "9.1.3",
"base64-loader": "1.0.0",