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:
@@ -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 }}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user