1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-24032] upgrade nav button updates (#16933)

* [PM-24032] upgrade nav button post-upgrade action

* fixing broken stories

* added component tests

* new behavior for the nav button when self hosted

* fix stories
This commit is contained in:
Kyle Denney
2025-10-21 11:18:15 -05:00
committed by GitHub
parent 93ab65cab9
commit 1794803deb
4 changed files with 220 additions and 5 deletions

View File

@@ -5,7 +5,7 @@
<button
type="button"
class="tw-py-1.5 tw-px-4 tw-flex tw-gap-2 tw-items-center tw-size-full focus-visible:tw-ring-2 focus-visible:tw-ring-offset-0 focus:tw-outline-none focus-visible:tw-outline-none focus-visible:tw-ring-text-alt2 focus-visible:tw-z-10 tw-font-semibold tw-rounded-full tw-transition tw-border tw-border-solid tw-text-left tw-bg-primary-100 tw-text-primary-600 tw-border-primary-600 hover:tw-bg-hover-default hover:tw-text-primary-700 hover:tw-border-primary-700"
(click)="openUpgradeDialog()"
(click)="upgrade()"
>
<i class="bwi bwi-premium" aria-hidden="true"></i>
{{ "upgradeYourPlan" | i18n }}

View File

@@ -0,0 +1,162 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { Router } from "@angular/router";
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject, of } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { UserId } from "@bitwarden/common/types/guid";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogRef, DialogService } from "@bitwarden/components";
import {
UnifiedUpgradeDialogResult,
UnifiedUpgradeDialogStatus,
} from "../../unified-upgrade-dialog/unified-upgrade-dialog.component";
import { UpgradeNavButtonComponent } from "./upgrade-nav-button.component";
describe("UpgradeNavButtonComponent", () => {
let component: UpgradeNavButtonComponent;
let fixture: ComponentFixture<UpgradeNavButtonComponent>;
let mockDialogService: MockProxy<DialogService>;
let mockAccountService: MockProxy<AccountService>;
let mockSyncService: MockProxy<SyncService>;
let mockApiService: MockProxy<ApiService>;
let mockRouter: MockProxy<Router>;
let mockI18nService: MockProxy<I18nService>;
let mockPlatformUtilsService: MockProxy<PlatformUtilsService>;
let activeAccount$: BehaviorSubject<Account | null>;
const mockAccount: Account = {
id: "user-id" as UserId,
email: "test@example.com",
emailVerified: true,
name: "Test User",
};
beforeEach(async () => {
mockDialogService = mock<DialogService>();
mockAccountService = mock<AccountService>();
mockSyncService = mock<SyncService>();
mockApiService = mock<ApiService>();
mockRouter = mock<Router>();
mockI18nService = mock<I18nService>();
mockPlatformUtilsService = mock<PlatformUtilsService>();
activeAccount$ = new BehaviorSubject<Account | null>(mockAccount);
mockAccountService.activeAccount$ = activeAccount$;
mockI18nService.t.mockImplementation((key) => key);
mockPlatformUtilsService.isSelfHost.mockReturnValue(false);
await TestBed.configureTestingModule({
imports: [UpgradeNavButtonComponent],
providers: [
{ provide: DialogService, useValue: mockDialogService },
{ provide: AccountService, useValue: mockAccountService },
{ provide: SyncService, useValue: mockSyncService },
{ provide: ApiService, useValue: mockApiService },
{ provide: Router, useValue: mockRouter },
{ provide: I18nService, useValue: mockI18nService },
{ provide: PlatformUtilsService, useValue: mockPlatformUtilsService },
],
}).compileComponents();
fixture = TestBed.createComponent(UpgradeNavButtonComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
describe("upgrade()", () => {
describe("when self-hosted", () => {
beforeEach(() => {
mockPlatformUtilsService.isSelfHost.mockReturnValue(true);
});
it("should navigate to subscription page", async () => {
await component.upgrade();
expect(mockRouter.navigate).toHaveBeenCalledWith(["/settings/subscription/premium"]);
expect(mockDialogService.open).not.toHaveBeenCalled();
});
});
describe("when not self-hosted", () => {
beforeEach(() => {
mockPlatformUtilsService.isSelfHost.mockReturnValue(false);
});
it("should return early if no active account exists", async () => {
activeAccount$.next(null);
await component.upgrade();
expect(mockDialogService.open).not.toHaveBeenCalled();
});
it("should open upgrade dialog with correct configuration", async () => {
const mockDialogRef = mock<DialogRef<UnifiedUpgradeDialogResult>>();
mockDialogRef.closed = of({ status: UnifiedUpgradeDialogStatus.Closed });
mockDialogService.open.mockReturnValue(mockDialogRef);
await component.upgrade();
expect(mockDialogService.open).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
data: {
account: mockAccount,
planSelectionStepTitleOverride: "upgradeYourPlan",
hideContinueWithoutUpgradingButton: true,
},
}),
);
});
it("should refresh token and sync after upgrading to premium", async () => {
const mockDialogRef = mock<DialogRef<UnifiedUpgradeDialogResult>>();
mockDialogRef.closed = of({ status: UnifiedUpgradeDialogStatus.UpgradedToPremium });
mockDialogService.open.mockReturnValue(mockDialogRef);
await component.upgrade();
expect(mockApiService.refreshIdentityToken).toHaveBeenCalled();
expect(mockSyncService.fullSync).toHaveBeenCalledWith(true);
});
it("should navigate to organization vault after upgrading to families", async () => {
const organizationId = "org-123";
const mockDialogRef = mock<DialogRef<UnifiedUpgradeDialogResult>>();
mockDialogRef.closed = of({
status: UnifiedUpgradeDialogStatus.UpgradedToFamilies,
organizationId,
});
mockDialogService.open.mockReturnValue(mockDialogRef);
await component.upgrade();
expect(mockRouter.navigate).toHaveBeenCalledWith([
`/organizations/${organizationId}/vault`,
]);
});
it("should do nothing when dialog closes without upgrade", async () => {
const mockDialogRef = mock<DialogRef<UnifiedUpgradeDialogResult>>();
mockDialogRef.closed = of({ status: UnifiedUpgradeDialogStatus.Closed });
mockDialogService.open.mockReturnValue(mockDialogRef);
await component.upgrade();
expect(mockApiService.refreshIdentityToken).not.toHaveBeenCalled();
expect(mockSyncService.fullSync).not.toHaveBeenCalled();
expect(mockRouter.navigate).not.toHaveBeenCalled();
});
});
});
});

View File

@@ -1,11 +1,18 @@
import { Component, inject } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom, lastValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService } from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
import { UnifiedUpgradeDialogComponent } from "../../unified-upgrade-dialog/unified-upgrade-dialog.component";
import {
UnifiedUpgradeDialogComponent,
UnifiedUpgradeDialogStatus,
} from "../../unified-upgrade-dialog/unified-upgrade-dialog.component";
@Component({
selector: "app-upgrade-nav-button",
@@ -16,8 +23,25 @@ import { UnifiedUpgradeDialogComponent } from "../../unified-upgrade-dialog/unif
export class UpgradeNavButtonComponent {
private dialogService = inject(DialogService);
private accountService = inject(AccountService);
private syncService = inject(SyncService);
private apiService = inject(ApiService);
private router = inject(Router);
private platformUtilsService = inject(PlatformUtilsService);
openUpgradeDialog = async () => {
upgrade = async () => {
if (this.platformUtilsService.isSelfHost()) {
await this.navigateToSelfHostSubscriptionPage();
} else {
await this.openUpgradeDialog();
}
};
private async navigateToSelfHostSubscriptionPage(): Promise<void> {
const subscriptionUrl = "/settings/subscription/premium";
await this.router.navigate([subscriptionUrl]);
}
private async openUpgradeDialog() {
const account = await firstValueFrom(this.accountService.activeAccount$);
if (!account) {
return;
@@ -31,6 +55,14 @@ export class UpgradeNavButtonComponent {
},
});
await lastValueFrom(dialogRef.closed);
};
const result = await lastValueFrom(dialogRef.closed);
if (result?.status === UnifiedUpgradeDialogStatus.UpgradedToPremium) {
await this.apiService.refreshIdentityToken();
await this.syncService.fullSync(true);
} else if (result?.status === UnifiedUpgradeDialogStatus.UpgradedToFamilies) {
const redirectUrl = `/organizations/${result.organizationId}/vault`;
await this.router.navigate([redirectUrl]);
}
}
}

View File

@@ -1,9 +1,12 @@
import { Meta, moduleMetadata, StoryObj } from "@storybook/angular";
import { of } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { UserId } from "@bitwarden/common/types/guid";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService, I18nMockService } from "@bitwarden/components";
import { UpgradeNavButtonComponent } from "@bitwarden/web-vault/app/billing/individual/upgrade/upgrade-nav-button/upgrade-nav-button/upgrade-nav-button.component";
@@ -40,6 +43,24 @@ export default {
}),
},
},
{
provide: ApiService,
useValue: {
refreshIdentityToken: () => {},
},
},
{
provide: SyncService,
useValue: {
fullSync: () => {},
},
},
{
provide: PlatformUtilsService,
useValue: {
isSelfHost: () => false,
},
},
],
}),
],