1
0
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:
Alex Morask
2024-01-23 12:47:52 -05:00
committed by GitHub
parent 4475f67bbc
commit 014281cb93
18 changed files with 208 additions and 8 deletions

View File

@@ -38,7 +38,7 @@ import {
],
})
export class Fido2UseBrowserLinkComponent {
showOverlay: boolean = false;
showOverlay = false;
isOpen = false;
overlayPosition: ConnectedPosition[] = [
{

View File

@@ -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>

View File

@@ -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,

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -1,3 +1,4 @@
<app-navbar></app-navbar>
<app-payment-method-banners></app-payment-method-banners>
<router-outlet></router-outlet>
<app-footer></app-footer>

View File

@@ -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,

View File

@@ -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.'"
}
}