Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions apps/web/src/app/auth/settings/account/profile.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,33 @@
<bit-form-field>
<bit-label>{{ "email" | i18n }}</bit-label>
<input bitInput [value]="email()" readonly />
@if (profile.emailVerified) {
<span
bitBadge
variant="success"
slot="inline-end"
class="tw-shrink-0 tw-whitespace-nowrap"
>
{{ "verified" | i18n }}
</span>
} @else {
<button
id="profile_button_verifyEmail"
type="button"
buttonType="secondary"
size="small"
startIcon="bwi-envelope"
bitButton
bitFormButton
appStopClick
appStopProp
slot="inline-end"
class="tw-shrink-0 tw-whitespace-nowrap"
[bitAction]="verifyEmail"
>
{{ "verifyEmail" | i18n }}
</button>
}
</bit-form-field>
</div>
<div class="tw-col-span-12 @3xl:tw-col-span-6 tw-row-start-1 @3xl:tw-row-start-auto">
Expand Down
109 changes: 109 additions & 0 deletions apps/web/src/app/auth/settings/account/profile.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { mock } from "jest-mock-extended";
import { of } from "rxjs";

import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
import { ProfileResponse } from "@bitwarden/common/models/response/profile.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { DialogService, ToastService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";

import { ProfileComponent } from "./profile.component";

describe("ProfileComponent", () => {
let component: ProfileComponent;
let fixture: ComponentFixture<ProfileComponent>;
let apiService: ReturnType<typeof mock<ApiService>>;
let toastService: ReturnType<typeof mock<ToastService>>;
let accountService: FakeAccountService;

const userId = "user-id" as UserId;

function buildProfile(emailVerified: boolean): ProfileResponse {
return new ProfileResponse({
Id: userId,
Name: "Test User",
Email: "test@bitwarden.com",
EmailVerified: emailVerified,
});
}

beforeEach(async () => {
apiService = mock<ApiService>();
toastService = mock<ToastService>();
accountService = mockAccountServiceWith(userId);

apiService.getProfile.mockResolvedValue(buildProfile(false));

await TestBed.configureTestingModule({
imports: [ProfileComponent],
providers: [
{ provide: ApiService, useValue: apiService },
{
provide: OrganizationService,
useValue: mock<OrganizationService>({ organizations$: () => of([]) }),
},
{ provide: AccountService, useValue: accountService },
{ provide: I18nService, useValue: { t: (key: string) => key } },
{ provide: LogService, useValue: mock<LogService>() },
{ provide: DialogService, useValue: mock<DialogService>() },
{ provide: ToastService, useValue: toastService },
{ provide: KeyService, useValue: mock<KeyService>({ userPublicKey$: () => of(null) }) },
{ provide: AvatarService, useValue: mock<AvatarService>({ avatarColor$: of(null) }) },
],
}).compileComponents();

fixture = TestBed.createComponent(ProfileComponent);
component = fixture.componentInstance;
});

describe("email verification indicator", () => {
it("shows the verified badge when the email is verified", async () => {
apiService.getProfile.mockResolvedValue(buildProfile(true));

await component.ngOnInit();
fixture.detectChanges();

const badge = fixture.debugElement.nativeElement.querySelector("[bitbadge]");
const verifyButton = fixture.debugElement.nativeElement.querySelector(
"#profile_button_verifyEmail",
);

expect(badge).not.toBeNull();
expect(verifyButton).toBeNull();
});

it("shows the verify email button when the email is not verified", async () => {
apiService.getProfile.mockResolvedValue(buildProfile(false));

await component.ngOnInit();
fixture.detectChanges();

const badge = fixture.debugElement.nativeElement.querySelector("[bitbadge]");
const verifyButton = fixture.debugElement.nativeElement.querySelector(
"#profile_button_verifyEmail",
);

expect(badge).toBeNull();
expect(verifyButton).not.toBeNull();
});
});

describe("verifyEmail", () => {
it("sends the verification email and shows a success toast", async () => {
await component["verifyEmail"]();

expect(apiService.postAccountVerifyEmail).toHaveBeenCalled();
expect(toastService.showToast).toHaveBeenCalledWith({
variant: "success",
message: "checkInboxForVerification",
});
});
});
});
8 changes: 8 additions & 0 deletions apps/web/src/app/auth/settings/account/profile.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,12 @@ export class ProfileComponent implements OnInit {
message: this.i18nService.t("accountUpdated"),
});
};

protected readonly verifyEmail = async () => {
await this.apiService.postAccountVerifyEmail();
Comment thread
JaredSnider-Bitwarden marked this conversation as resolved.
Outdated
this.toastService.showToast({
variant: "success",
message: this.i18nService.t("checkInboxForVerification"),
});
};
}
13 changes: 0 additions & 13 deletions apps/web/src/app/auth/settings/verify-email.component.html

This file was deleted.

67 changes: 0 additions & 67 deletions apps/web/src/app/auth/settings/verify-email.component.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,13 @@ import { TestBed } from "@angular/core/testing";
import { BehaviorSubject } from "rxjs";

import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { DeviceType } from "@bitwarden/common/enums";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { StateProvider } from "@bitwarden/common/platform/state";
import {
FakeStateProvider,
mockAccountServiceWith,
mockAccountInfoWith,
} from "@bitwarden/common/spec";
import { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";

Expand All @@ -25,20 +20,12 @@ describe("VaultBannersService", () => {
const hasPremiumFromAnySource$ = new BehaviorSubject<boolean>(false);
const userId = Utils.newGuid() as UserId;
const fakeStateProvider = new FakeStateProvider(mockAccountServiceWith(userId));
const getEmailVerified = jest.fn().mockResolvedValue(true);
const lastSync$ = new BehaviorSubject<Date | null>(null);
const accounts$ = new BehaviorSubject({
[userId]: mockAccountInfoWith({
email: "test@bitwarden.com",
name: "name",
}),
});
const pendingAuthRequests$ = new BehaviorSubject<Array<AuthRequestResponse>>([]);

beforeEach(() => {
lastSync$.next(new Date("2024-05-14"));
isSelfHost.mockClear();
getEmailVerified.mockClear().mockResolvedValue(true);

TestBed.configureTestingModule({
providers: [
Expand All @@ -55,10 +42,6 @@ describe("VaultBannersService", () => {
provide: StateProvider,
useValue: fakeStateProvider,
},
{
provide: AccountService,
useValue: { accounts$ },
},
{
provide: SyncService,
useValue: { lastSync$: () => lastSync$ },
Expand Down Expand Up @@ -102,33 +85,6 @@ describe("VaultBannersService", () => {
});
});

describe("VerifyEmail", () => {
beforeEach(async () => {
accounts$.next({
[userId]: {
...accounts$.value[userId],
emailVerified: false,
},
});
});

it("shows verify email banner", async () => {
service = TestBed.inject(VaultBannersService);

expect(await service.shouldShowVerifyEmailBanner(userId)).toBe(true);
});

it("dismisses verify email banner", async () => {
service = TestBed.inject(VaultBannersService);

expect(await service.shouldShowVerifyEmailBanner(userId)).toBe(true);

await service.dismissBanner(userId, VisibleVaultBanner.VerifyEmail);

expect(await service.shouldShowVerifyEmailBanner(userId)).toBe(false);
});
});

describe("PendingAuthRequest", () => {
const now = new Date();
let authRequestResponse: AuthRequestResponse;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Injectable } from "@angular/core";
import { firstValueFrom, map } from "rxjs";
import { firstValueFrom } from "rxjs";

import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import {
Expand All @@ -17,7 +16,6 @@ import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values";

export const VisibleVaultBanner = {
OutdatedBrowser: "outdated-browser",
VerifyEmail: "verify-email",
PendingAuthRequest: "pending-auth-request",
} as const;

Expand All @@ -38,7 +36,6 @@ export const BANNERS_DISMISSED_DISK_KEY = new UserKeyDefinition<SessionBanners[]
@Injectable()
export class VaultBannersService {
constructor(
private accountService: AccountService,
private stateProvider: StateProvider,
private billingAccountProfileStateService: BillingAccountProfileStateService,
private platformUtilsService: PlatformUtilsService,
Expand Down Expand Up @@ -69,19 +66,6 @@ export class VaultBannersService {
return outdatedBrowser && !alreadyDismissed;
}

/** Returns true when the verify email banner should be shown */
async shouldShowVerifyEmailBanner(userId: UserId): Promise<boolean> {
const needsVerification = !(
await firstValueFrom(this.accountService.accounts$.pipe(map((accounts) => accounts[userId])))
)?.emailVerified;

const alreadyDismissed = (await this.getBannerDismissedState(userId)).includes(
VisibleVaultBanner.VerifyEmail,
);

return needsVerification && !alreadyDismissed;
}

/** Dismiss the given banner and perform any respective side effects */
async dismissBanner(userId: UserId, banner: SessionBanners): Promise<void> {
await this.sessionBannerState(userId).update((current) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,3 @@
</a>
</bit-banner>
}

@if (visibleBanners.includes(VisibleVaultBanner.VerifyEmail)) {
<app-verify-email
id="verify-email-banner"
(onDismiss)="dismissBanner(VisibleVaultBanner.VerifyEmail)"
(onVerified)="dismissBanner(VisibleVaultBanner.VerifyEmail)"
></app-verify-email>
}
Loading
Loading