mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-22 16:29:09 +01:00
[PM-6975] Replace purchasedPremium broadcast message with observables (#8421)
In https://github.com/bitwarden/clients/pull/8133 the premium state changed to be derived from observables, which means we can get rid of the `purchasePremium` messages that are sent and instead rely directly on the observable to distribute the state.
This commit is contained in:
parent
daa9e742e7
commit
23c89bda74
@ -124,10 +124,7 @@ export class PremiumComponent implements OnInit {
|
|||||||
await this.apiService.refreshIdentityToken();
|
await this.apiService.refreshIdentityToken();
|
||||||
await this.syncService.fullSync(true);
|
await this.syncService.fullSync(true);
|
||||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("premiumUpdated"));
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("premiumUpdated"));
|
||||||
this.messagingService.send("purchasedPremium");
|
await this.router.navigate(["/settings/subscription/user-subscription"]);
|
||||||
// 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.router.navigate(["/settings/subscription/user-subscription"]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get additionalStorageTotal(): number {
|
get additionalStorageTotal(): number {
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
<bit-nav-item
|
<bit-nav-item
|
||||||
[text]="'subscription' | i18n"
|
[text]="'subscription' | i18n"
|
||||||
route="settings/subscription"
|
route="settings/subscription"
|
||||||
*ngIf="!hideSubscription"
|
*ngIf="showSubscription$ | async"
|
||||||
></bit-nav-item>
|
></bit-nav-item>
|
||||||
<bit-nav-item [text]="'domainRules' | i18n" route="settings/domain-rules"></bit-nav-item>
|
<bit-nav-item [text]="'domainRules' | i18n" route="settings/domain-rules"></bit-nav-item>
|
||||||
<bit-nav-item
|
<bit-nav-item
|
||||||
@ -29,7 +29,7 @@
|
|||||||
<bit-nav-item
|
<bit-nav-item
|
||||||
[text]="'sponsoredFamilies' | i18n"
|
[text]="'sponsoredFamilies' | i18n"
|
||||||
route="settings/sponsored-families"
|
route="settings/sponsored-families"
|
||||||
*ngIf="hasFamilySponsorshipAvailable"
|
*ngIf="hasFamilySponsorshipAvailable$ | async"
|
||||||
></bit-nav-item>
|
></bit-nav-item>
|
||||||
</bit-nav-group>
|
</bit-nav-group>
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
import { RouterModule } from "@angular/router";
|
import { RouterModule } from "@angular/router";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { Observable, combineLatest, concatMap } from "rxjs";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
@ -18,8 +17,6 @@ import { PaymentMethodWarningsModule } from "../billing/shared";
|
|||||||
|
|
||||||
import { PasswordManagerLogo } from "./password-manager-logo";
|
import { PasswordManagerLogo } from "./password-manager-logo";
|
||||||
|
|
||||||
const BroadcasterSubscriptionId = "UserLayoutComponent";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-user-layout",
|
selector: "app-user-layout",
|
||||||
templateUrl: "user-layout.component.html",
|
templateUrl: "user-layout.component.html",
|
||||||
@ -34,10 +31,10 @@ const BroadcasterSubscriptionId = "UserLayoutComponent";
|
|||||||
PaymentMethodWarningsModule,
|
PaymentMethodWarningsModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class UserLayoutComponent implements OnInit, OnDestroy {
|
export class UserLayoutComponent implements OnInit {
|
||||||
protected readonly logo = PasswordManagerLogo;
|
protected readonly logo = PasswordManagerLogo;
|
||||||
hasFamilySponsorshipAvailable: boolean;
|
protected hasFamilySponsorshipAvailable$: Observable<boolean>;
|
||||||
hideSubscription: boolean;
|
protected showSubscription$: Observable<boolean>;
|
||||||
|
|
||||||
protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$(
|
protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$(
|
||||||
FeatureFlag.ShowPaymentMethodWarningBanners,
|
FeatureFlag.ShowPaymentMethodWarningBanners,
|
||||||
@ -45,8 +42,6 @@ export class UserLayoutComponent implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private broadcasterService: BroadcasterService,
|
|
||||||
private ngZone: NgZone,
|
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
@ -58,43 +53,28 @@ export class UserLayoutComponent implements OnInit, OnDestroy {
|
|||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
document.body.classList.remove("layout_frontend");
|
document.body.classList.remove("layout_frontend");
|
||||||
|
|
||||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
|
||||||
// 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.ngZone.run(async () => {
|
|
||||||
switch (message.command) {
|
|
||||||
case "purchasedPremium":
|
|
||||||
await this.load();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.syncService.fullSync(false);
|
await this.syncService.fullSync(false);
|
||||||
await this.load();
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
this.hasFamilySponsorshipAvailable$ = this.organizationService.canManageSponsorships$;
|
||||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async load() {
|
// We want to hide the subscription menu for organizations that provide premium.
|
||||||
const hasPremiumPersonally = await firstValueFrom(
|
// Except if the user has premium personally or has a billing history.
|
||||||
|
this.showSubscription$ = combineLatest([
|
||||||
this.billingAccountProfileStateService.hasPremiumPersonally$,
|
this.billingAccountProfileStateService.hasPremiumPersonally$,
|
||||||
);
|
|
||||||
const hasPremiumFromOrg = await firstValueFrom(
|
|
||||||
this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$,
|
this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$,
|
||||||
);
|
]).pipe(
|
||||||
const selfHosted = this.platformUtilsService.isSelfHost();
|
concatMap(async ([hasPremiumPersonally, hasPremiumFromOrg]) => {
|
||||||
|
const isCloud = !this.platformUtilsService.isSelfHost();
|
||||||
|
|
||||||
this.hasFamilySponsorshipAvailable = await this.organizationService.canManageSponsorships();
|
let billing = null;
|
||||||
let billing = null;
|
if (isCloud) {
|
||||||
if (!selfHosted) {
|
// TODO: We should remove the need to call this!
|
||||||
// TODO: We should remove the need to call this!
|
billing = await this.apiService.getUserBillingHistory();
|
||||||
billing = await this.apiService.getUserBillingHistory();
|
}
|
||||||
}
|
|
||||||
this.hideSubscription =
|
const cloudAndBillingHistory = isCloud && !billing?.hasNoHistory;
|
||||||
!hasPremiumPersonally && hasPremiumFromOrg && (selfHosted || billing?.hasNoHistory);
|
return hasPremiumPersonally || !hasPremiumFromOrg || cloudAndBillingHistory;
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,68 +0,0 @@
|
|||||||
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
|
||||||
import { firstValueFrom } from "rxjs";
|
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
|
||||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
|
||||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
|
||||||
|
|
||||||
const BroadcasterSubscriptionId = "SettingsComponent";
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: "app-settings",
|
|
||||||
templateUrl: "settings.component.html",
|
|
||||||
})
|
|
||||||
export class SettingsComponent implements OnInit, OnDestroy {
|
|
||||||
premium: boolean;
|
|
||||||
selfHosted: boolean;
|
|
||||||
hasFamilySponsorshipAvailable: boolean;
|
|
||||||
hideSubscription: boolean;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private broadcasterService: BroadcasterService,
|
|
||||||
private ngZone: NgZone,
|
|
||||||
private platformUtilsService: PlatformUtilsService,
|
|
||||||
private organizationService: OrganizationService,
|
|
||||||
private apiService: ApiService,
|
|
||||||
private billingAccountProfileStateServiceAbstraction: BillingAccountProfileStateService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async ngOnInit() {
|
|
||||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
|
||||||
// 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.ngZone.run(async () => {
|
|
||||||
switch (message.command) {
|
|
||||||
case "purchasedPremium":
|
|
||||||
await this.load();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.selfHosted = await this.platformUtilsService.isSelfHost();
|
|
||||||
await this.load();
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
|
||||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async load() {
|
|
||||||
this.premium = await firstValueFrom(
|
|
||||||
this.billingAccountProfileStateServiceAbstraction.hasPremiumPersonally$,
|
|
||||||
);
|
|
||||||
this.hasFamilySponsorshipAvailable = await this.organizationService.canManageSponsorships();
|
|
||||||
const hasPremiumFromOrg = await firstValueFrom(
|
|
||||||
this.billingAccountProfileStateServiceAbstraction.hasPremiumFromAnyOrganization$,
|
|
||||||
);
|
|
||||||
let billing = null;
|
|
||||||
if (!this.selfHosted) {
|
|
||||||
billing = await this.apiService.getUserBillingHistory();
|
|
||||||
}
|
|
||||||
this.hideSubscription =
|
|
||||||
!this.premium && hasPremiumFromOrg && (this.selfHosted || billing?.hasNoHistory);
|
|
||||||
}
|
|
||||||
}
|
|
@ -116,7 +116,7 @@ export abstract class OrganizationService {
|
|||||||
* https://bitwarden.atlassian.net/browse/AC-2252.
|
* https://bitwarden.atlassian.net/browse/AC-2252.
|
||||||
*/
|
*/
|
||||||
getFromState: (id: string) => Promise<Organization>;
|
getFromState: (id: string) => Promise<Organization>;
|
||||||
canManageSponsorships: () => Promise<boolean>;
|
canManageSponsorships$: Observable<boolean>;
|
||||||
hasOrganizations: () => Promise<boolean>;
|
hasOrganizations: () => Promise<boolean>;
|
||||||
get$: (id: string) => Observable<Organization | undefined>;
|
get$: (id: string) => Observable<Organization | undefined>;
|
||||||
get: (id: string) => Promise<Organization>;
|
get: (id: string) => Promise<Organization>;
|
||||||
|
@ -121,7 +121,7 @@ describe("OrganizationService", () => {
|
|||||||
const mockData: OrganizationData[] = buildMockOrganizations(1);
|
const mockData: OrganizationData[] = buildMockOrganizations(1);
|
||||||
mockData[0].familySponsorshipAvailable = true;
|
mockData[0].familySponsorshipAvailable = true;
|
||||||
fakeActiveUserState.nextState(arrayToRecord(mockData));
|
fakeActiveUserState.nextState(arrayToRecord(mockData));
|
||||||
const result = await organizationService.canManageSponsorships();
|
const result = await firstValueFrom(organizationService.canManageSponsorships$);
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -129,7 +129,7 @@ describe("OrganizationService", () => {
|
|||||||
const mockData: OrganizationData[] = buildMockOrganizations(1);
|
const mockData: OrganizationData[] = buildMockOrganizations(1);
|
||||||
mockData[0].familySponsorshipFriendlyName = "Something";
|
mockData[0].familySponsorshipFriendlyName = "Something";
|
||||||
fakeActiveUserState.nextState(arrayToRecord(mockData));
|
fakeActiveUserState.nextState(arrayToRecord(mockData));
|
||||||
const result = await organizationService.canManageSponsorships();
|
const result = await firstValueFrom(organizationService.canManageSponsorships$);
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -137,7 +137,7 @@ describe("OrganizationService", () => {
|
|||||||
const mockData: OrganizationData[] = buildMockOrganizations(1);
|
const mockData: OrganizationData[] = buildMockOrganizations(1);
|
||||||
mockData[0].familySponsorshipFriendlyName = null;
|
mockData[0].familySponsorshipFriendlyName = null;
|
||||||
fakeActiveUserState.nextState(arrayToRecord(mockData));
|
fakeActiveUserState.nextState(arrayToRecord(mockData));
|
||||||
const result = await organizationService.canManageSponsorships();
|
const result = await firstValueFrom(organizationService.canManageSponsorships$);
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -77,14 +77,10 @@ export class OrganizationService implements InternalOrganizationServiceAbstracti
|
|||||||
return await firstValueFrom(this.getOrganizationsFromState$(userId as UserId));
|
return await firstValueFrom(this.getOrganizationsFromState$(userId as UserId));
|
||||||
}
|
}
|
||||||
|
|
||||||
async canManageSponsorships(): Promise<boolean> {
|
canManageSponsorships$ = this.organizations$.pipe(
|
||||||
return await firstValueFrom(
|
mapToExcludeOrganizationsWithoutFamilySponsorshipSupport(),
|
||||||
this.organizations$.pipe(
|
mapToBooleanHasAnyOrganizations(),
|
||||||
mapToExcludeOrganizationsWithoutFamilySponsorshipSupport(),
|
);
|
||||||
mapToBooleanHasAnyOrganizations(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async hasOrganizations(): Promise<boolean> {
|
async hasOrganizations(): Promise<boolean> {
|
||||||
return await firstValueFrom(this.organizations$.pipe(mapToBooleanHasAnyOrganizations()));
|
return await firstValueFrom(this.organizations$.pipe(mapToBooleanHasAnyOrganizations()));
|
||||||
|
Loading…
Reference in New Issue
Block a user