mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 07:43:35 +00:00
[AC-1758] Show banner when organization requires a payment method (#7088)
* Add billing banner states to account settings * Add billing banner service * Add add-payment-method-banners.component * Use add-payment-method-banners.component in layouts * Clear banner on payment method addition * Ran prettier after CI update * Finalize banners styling/translations * Will's (non-Tailwind) feedback * Review feedback * Review feedback * Review feedback * Replace StateService with StateProvider in BillingBannerService * Remove StateService methods
This commit is contained in:
@@ -38,7 +38,7 @@ import {
|
||||
],
|
||||
})
|
||||
export class Fido2UseBrowserLinkComponent {
|
||||
showOverlay: boolean = false;
|
||||
showOverlay = false;
|
||||
isOpen = false;
|
||||
overlayPosition: ConnectedPosition[] = [
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<app-navbar></app-navbar>
|
||||
<app-payment-method-banners></app-payment-method-banners>
|
||||
<div class="org-nav !tw-h-32" *ngIf="organization$ | async as organization">
|
||||
<div class="container d-flex">
|
||||
<div class="d-flex flex-column">
|
||||
@@ -36,6 +37,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
<app-footer></app-footer>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Component, EventEmitter, Input, Output, ViewChild } from "@angular/core
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { BillingBannerServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-banner.service.abstraction";
|
||||
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
|
||||
import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -33,6 +34,7 @@ export class AdjustPaymentComponent {
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private logService: LogService,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private billingBannerService: BillingBannerServiceAbstraction,
|
||||
) {}
|
||||
|
||||
async submit() {
|
||||
@@ -56,6 +58,9 @@ export class AdjustPaymentComponent {
|
||||
}
|
||||
});
|
||||
await this.formPromise;
|
||||
if (this.organizationId) {
|
||||
await this.billingBannerService.setPaymentMethodBannerState(this.organizationId, false);
|
||||
}
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<ng-container *ngFor="let banner of banners$ | async">
|
||||
<bit-banner
|
||||
*ngIf="banner.visible"
|
||||
bannerType="warning"
|
||||
(onClose)="closeBanner(banner.organizationId)"
|
||||
>
|
||||
{{ "maintainYourSubscription" | i18n: banner.organizationName }}
|
||||
<a
|
||||
bitLink
|
||||
linkType="contrast"
|
||||
[routerLink]="['/organizations', banner.organizationId, 'billing', 'payment-method']"
|
||||
>{{ "addAPaymentMethod" | i18n }}</a
|
||||
>.
|
||||
</bit-banner>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { combineLatest, Observable, switchMap } from "rxjs";
|
||||
|
||||
import { OrganizationApiServiceAbstraction as OrganizationApiService } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import {
|
||||
OrganizationService,
|
||||
canAccessAdmin,
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { BillingBannerServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-banner.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { BannerModule } from "@bitwarden/components";
|
||||
|
||||
import { SharedModule } from "../../shared/shared.module";
|
||||
|
||||
type PaymentMethodBannerData = {
|
||||
organizationId: string;
|
||||
organizationName: string;
|
||||
visible: boolean;
|
||||
};
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "app-payment-method-banners",
|
||||
templateUrl: "payment-method-banners.component.html",
|
||||
imports: [BannerModule, SharedModule],
|
||||
})
|
||||
export class PaymentMethodBannersComponent {
|
||||
constructor(
|
||||
private billingBannerService: BillingBannerServiceAbstraction,
|
||||
private i18nService: I18nService,
|
||||
private organizationService: OrganizationService,
|
||||
private organizationApiService: OrganizationApiService,
|
||||
) {}
|
||||
|
||||
private organizations$ = this.organizationService.memberOrganizations$.pipe(
|
||||
canAccessAdmin(this.i18nService),
|
||||
);
|
||||
|
||||
protected banners$: Observable<PaymentMethodBannerData[]> = combineLatest([
|
||||
this.organizations$,
|
||||
this.billingBannerService.paymentMethodBannerStates$,
|
||||
]).pipe(
|
||||
switchMap(async ([organizations, paymentMethodBannerStates]) => {
|
||||
return await Promise.all(
|
||||
organizations.map(async (organization) => {
|
||||
const matchingBanner = paymentMethodBannerStates.find(
|
||||
(banner) => banner.organizationId === organization.id,
|
||||
);
|
||||
if (matchingBanner !== null && matchingBanner !== undefined) {
|
||||
return {
|
||||
organizationId: organization.id,
|
||||
organizationName: organization.name,
|
||||
visible: matchingBanner.visible,
|
||||
};
|
||||
}
|
||||
const response = await this.organizationApiService.risksSubscriptionFailure(
|
||||
organization.id,
|
||||
);
|
||||
await this.billingBannerService.setPaymentMethodBannerState(
|
||||
organization.id,
|
||||
response.risksSubscriptionFailure,
|
||||
);
|
||||
return {
|
||||
organizationId: organization.id,
|
||||
organizationName: organization.name,
|
||||
visible: response.risksSubscriptionFailure,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
protected async closeBanner(organizationId: string): Promise<void> {
|
||||
await this.billingBannerService.setPaymentMethodBannerState(organizationId, false);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
<app-navbar></app-navbar>
|
||||
<app-payment-method-banners></app-payment-method-banners>
|
||||
<router-outlet></router-outlet>
|
||||
<app-footer></app-footer>
|
||||
|
||||
@@ -60,6 +60,7 @@ import { UpdateTempPasswordComponent } from "../auth/update-temp-password.compon
|
||||
import { VerifyEmailTokenComponent } from "../auth/verify-email-token.component";
|
||||
import { VerifyRecoverDeleteComponent } from "../auth/verify-recover-delete.component";
|
||||
import { DynamicAvatarComponent } from "../components/dynamic-avatar.component";
|
||||
import { PaymentMethodBannersComponent } from "../components/payment-method-banners/payment-method-banners.component";
|
||||
import { SelectableAvatarComponent } from "../components/selectable-avatar.component";
|
||||
import { FooterComponent } from "../layouts/footer.component";
|
||||
import { FrontendLayoutComponent } from "../layouts/frontend-layout.component";
|
||||
@@ -109,6 +110,7 @@ import { SharedModule } from "./shared.module";
|
||||
PipesModule,
|
||||
PasswordCalloutComponent,
|
||||
DangerZoneComponent,
|
||||
PaymentMethodBannersComponent,
|
||||
],
|
||||
declarations: [
|
||||
AcceptFamilySponsorshipComponent,
|
||||
|
||||
@@ -7392,12 +7392,6 @@
|
||||
"skipToContent": {
|
||||
"message": "Skip to content"
|
||||
},
|
||||
"customBillingStart": {
|
||||
"message": "Custom billing is not reflected. Visit the "
|
||||
},
|
||||
"customBillingEnd": {
|
||||
"message": " page for latest invoicing."
|
||||
},
|
||||
"managePermissionRequired": {
|
||||
"message": "At least one member or group must have can manage permission."
|
||||
},
|
||||
@@ -7453,5 +7447,19 @@
|
||||
"commonImportFormats": {
|
||||
"message": "Common formats",
|
||||
"description": "Label indicating the most common import formats"
|
||||
},
|
||||
"maintainYourSubscription": {
|
||||
"message": "To maintain your subscription for $ORG$, ",
|
||||
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To maintain your subscription for $ORG$, add a payment method.'",
|
||||
"placeholders": {
|
||||
"org": {
|
||||
"content": "$1",
|
||||
"example": "Example Inc."
|
||||
}
|
||||
}
|
||||
},
|
||||
"addAPaymentMethod": {
|
||||
"message": "add a payment method",
|
||||
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To maintain your subscription for $ORG$, add a payment method.'"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user