diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.html b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.html index 4f7990f11a3..4409ab56d60 100644 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.html +++ b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.html @@ -2,7 +2,7 @@ { this.taxInformation = new TaxInformation(); }); + } else if (this.providerId) { + this.billingApiService + .getProviderTaxInformation(this.providerId) + .then((response) => (this.taxInformation = TaxInformation.from(response))) + .catch(() => { + this.taxInformation = new TaxInformation(); + }); } else { this.apiService .getTaxInfo() @@ -104,10 +114,12 @@ export class AdjustPaymentDialogComponent implements OnInit { } try { - if (!this.organizationId) { - await this.updatePremiumUserPaymentMethod(); - } else { + if (this.organizationId) { await this.updateOrganizationPaymentMethod(); + } else if (this.providerId) { + await this.updateProviderPaymentMethod(); + } else { + await this.updatePremiumUserPaymentMethod(); } this.toastService.showToast({ @@ -137,20 +149,6 @@ export class AdjustPaymentDialogComponent implements OnInit { await this.billingApiService.updateOrganizationPaymentMethod(this.organizationId, request); }; - protected get showTaxIdField(): boolean { - if (!this.organizationId) { - return false; - } - - switch (this.productTier) { - case ProductTierType.Free: - case ProductTierType.Families: - return false; - default: - return true; - } - } - private updatePremiumUserPaymentMethod = async () => { const { type, token } = await this.paymentComponent.tokenize(); @@ -168,6 +166,30 @@ export class AdjustPaymentDialogComponent implements OnInit { await this.apiService.postAccountPayment(request); }; + private updateProviderPaymentMethod = async () => { + const paymentSource = await this.paymentComponent.tokenize(); + + const request = new UpdatePaymentMethodRequest(); + request.paymentSource = paymentSource; + request.taxInformation = ExpandedTaxInfoUpdateRequest.From(this.taxInformation); + + await this.billingApiService.updateProviderPaymentMethod(this.providerId, request); + }; + + protected get showTaxIdField(): boolean { + if (this.organizationId) { + switch (this.productTier) { + case ProductTierType.Free: + case ProductTierType.Families: + return false; + default: + return true; + } + } else { + return !!this.providerId; + } + } + static open = ( dialogService: DialogService, dialogConfig: DialogConfig, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts index c720a94d5ec..9b5559af946 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts @@ -7,6 +7,7 @@ import { JslibModule } from "@bitwarden/angular/jslib.module"; import { SearchModule } from "@bitwarden/components"; import { DangerZoneComponent } from "@bitwarden/web-vault/app/auth/settings/account/danger-zone.component"; import { OrganizationPlansComponent } from "@bitwarden/web-vault/app/billing"; +import { VerifyBankAccountComponent } from "@bitwarden/web-vault/app/billing/shared/verify-bank-account/verify-bank-account.component"; import { OssModule } from "@bitwarden/web-vault/app/oss.module"; import { @@ -49,6 +50,7 @@ import { VerifyRecoverDeleteProviderComponent } from "./verify-recover-delete-pr ProvidersLayoutComponent, DangerZoneComponent, ScrollingModule, + VerifyBankAccountComponent, ], declarations: [ AcceptProviderComponent, diff --git a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html index 63b5bc01dd8..f2f72fa5bb4 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html +++ b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html @@ -63,15 +63,40 @@ - + {{ "accountCredit" | i18n }} {{ subscription.accountCredit | currency: "$" }} {{ "creditAppliedDesc" | i18n }} - + + + + {{ "paymentMethod" | i18n }} + + {{ "noPaymentMethod" | i18n }} + + + + + + + {{ subscription.paymentSource.description }} + - {{ "unverified" | i18n }} + + + + {{ updatePaymentSourceButtonText }} + + - + {{ "taxInformation" | i18n }} {{ "taxInformationDesc" | i18n }} - + diff --git a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts index df8a85e3e42..3d9388877fd 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts @@ -2,17 +2,26 @@ // @ts-strict-ignore import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { Subject, concatMap, takeUntil } from "rxjs"; +import { concatMap, lastValueFrom, Subject, takeUntil } from "rxjs"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; +import { PaymentMethodType } from "@bitwarden/common/billing/enums"; import { TaxInformation } from "@bitwarden/common/billing/models/domain"; import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; +import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request"; import { ProviderPlanResponse, ProviderSubscriptionResponse, } from "@bitwarden/common/billing/models/response/provider-subscription-response"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService, ToastService } from "@bitwarden/components"; import { BillingNotificationService } from "@bitwarden/web-vault/app/billing/services/billing-notification.service"; +import { + AdjustPaymentDialogComponent, + AdjustPaymentDialogResultType, +} from "@bitwarden/web-vault/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component"; @Component({ selector: "app-provider-subscription", @@ -29,11 +38,18 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy { protected readonly TaxInformation = TaxInformation; + protected readonly allowProviderPaymentMethod$ = this.configService.getFeatureFlag$( + FeatureFlag.PM18794_ProviderPaymentMethod, + ); + constructor( private billingApiService: BillingApiServiceAbstraction, private i18nService: I18nService, private route: ActivatedRoute, private billingNotificationService: BillingNotificationService, + private dialogService: DialogService, + private toastService: ToastService, + private configService: ConfigService, ) {} async ngOnInit() { @@ -66,6 +82,21 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy { } } + protected updatePaymentMethod = async (): Promise => { + const dialogRef = AdjustPaymentDialogComponent.open(this.dialogService, { + data: { + initialPaymentMethod: this.subscription.paymentSource?.type, + providerId: this.providerId, + }, + }); + + const result = await lastValueFrom(dialogRef.closed); + + if (result === AdjustPaymentDialogResultType.Submitted) { + await this.load(); + } + }; + protected updateTaxInformation = async (taxInformation: TaxInformation) => { try { const request = ExpandedTaxInfoUpdateRequest.From(taxInformation); @@ -76,6 +107,15 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy { } }; + protected verifyBankAccount = async (request: VerifyBankAccountRequest): Promise => { + await this.billingApiService.verifyProviderBankAccount(this.providerId, request); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("verifiedBankAccount"), + }); + }; + protected getFormattedCost( cost: number, seatMinimum: number, @@ -133,4 +173,28 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy { return "month"; } } + + protected get paymentSourceClasses() { + if (this.subscription.paymentSource == null) { + return []; + } + switch (this.subscription.paymentSource.type) { + case PaymentMethodType.Card: + return ["bwi-credit-card"]; + case PaymentMethodType.BankAccount: + return ["bwi-bank"]; + case PaymentMethodType.Check: + return ["bwi-money"]; + case PaymentMethodType.PayPal: + return ["bwi-paypal text-primary"]; + default: + return []; + } + } + + protected get updatePaymentSourceButtonText(): string { + const key = + this.subscription.paymentSource == null ? "addPaymentMethod" : "changePaymentMethod"; + return this.i18nService.t(key); + } } diff --git a/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts b/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts index 928f65a3636..21089933a59 100644 --- a/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts @@ -1,6 +1,8 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response"; + import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; import { ProviderOrganizationOrganizationDetailsResponse } from "../../admin-console/models/response/provider/provider-organization.response"; import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request"; @@ -50,6 +52,8 @@ export abstract class BillingApiServiceAbstraction { getProviderSubscription: (providerId: string) => Promise; + getProviderTaxInformation: (providerId: string) => Promise; + updateOrganizationPaymentMethod: ( organizationId: string, request: UpdatePaymentMethodRequest, @@ -66,6 +70,11 @@ export abstract class BillingApiServiceAbstraction { request: UpdateClientOrganizationRequest, ) => Promise; + updateProviderPaymentMethod: ( + providerId: string, + request: UpdatePaymentMethodRequest, + ) => Promise; + updateProviderTaxInformation: ( providerId: string, request: ExpandedTaxInfoUpdateRequest, @@ -76,6 +85,11 @@ export abstract class BillingApiServiceAbstraction { request: VerifyBankAccountRequest, ) => Promise; + verifyProviderBankAccount: ( + providerId: string, + request: VerifyBankAccountRequest, + ) => Promise; + restartSubscription: ( organizationId: string, request: OrganizationCreateRequest, diff --git a/libs/common/src/billing/models/response/provider-subscription-response.ts b/libs/common/src/billing/models/response/provider-subscription-response.ts index 4481f7588ff..d861a8a9d46 100644 --- a/libs/common/src/billing/models/response/provider-subscription-response.ts +++ b/libs/common/src/billing/models/response/provider-subscription-response.ts @@ -1,3 +1,5 @@ +import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response"; + import { ProviderType } from "../../../admin-console/enums"; import { BaseResponse } from "../../../models/response/base.response"; import { PlanType, ProductTierType } from "../../enums"; @@ -16,6 +18,7 @@ export class ProviderSubscriptionResponse extends BaseResponse { cancelAt?: string; suspension?: SubscriptionSuspensionResponse; providerType: ProviderType; + paymentSource?: PaymentSourceResponse; constructor(response: any) { super(response); @@ -38,6 +41,10 @@ export class ProviderSubscriptionResponse extends BaseResponse { this.suspension = new SubscriptionSuspensionResponse(suspension); } this.providerType = this.getResponseProperty("providerType"); + const paymentSource = this.getResponseProperty("paymentSource"); + if (paymentSource != null) { + this.paymentSource = new PaymentSourceResponse(paymentSource); + } } } diff --git a/libs/common/src/billing/services/billing-api.service.ts b/libs/common/src/billing/services/billing-api.service.ts index e7552b24d24..2292f26e616 100644 --- a/libs/common/src/billing/services/billing-api.service.ts +++ b/libs/common/src/billing/services/billing-api.service.ts @@ -1,6 +1,8 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response"; + import { ApiService } from "../../abstractions/api.service"; import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; import { ProviderOrganizationOrganizationDetailsResponse } from "../../admin-console/models/response/provider/provider-organization.response"; @@ -143,6 +145,17 @@ export class BillingApiService implements BillingApiServiceAbstraction { return new ProviderSubscriptionResponse(response); } + async getProviderTaxInformation(providerId: string): Promise { + const response = await this.apiService.send( + "GET", + "/providers/" + providerId + "/billing/tax-information", + null, + true, + true, + ); + return new TaxInfoResponse(response); + } + async updateOrganizationPaymentMethod( organizationId: string, request: UpdatePaymentMethodRequest, @@ -183,6 +196,19 @@ export class BillingApiService implements BillingApiServiceAbstraction { ); } + async updateProviderPaymentMethod( + providerId: string, + request: UpdatePaymentMethodRequest, + ): Promise { + return await this.apiService.send( + "PUT", + "/providers/" + providerId + "/billing/payment-method", + request, + true, + false, + ); + } + async updateProviderTaxInformation(providerId: string, request: ExpandedTaxInfoUpdateRequest) { return await this.apiService.send( "PUT", @@ -206,6 +232,19 @@ export class BillingApiService implements BillingApiServiceAbstraction { ); } + async verifyProviderBankAccount( + providerId: string, + request: VerifyBankAccountRequest, + ): Promise { + return await this.apiService.send( + "POST", + "/providers/" + providerId + "/billing/payment-method/verify-bank-account", + request, + true, + false, + ); + } + async restartSubscription( organizationId: string, request: OrganizationCreateRequest, diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index bbddb305937..c6f9c7daceb 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -46,6 +46,7 @@ export enum FeatureFlag { PM15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal", RecoveryCodeLogin = "pm-17128-recovery-code-login", PM12276_BreadcrumbEventLogs = "pm-12276-breadcrumbing-for-business-features", + PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -102,6 +103,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PM15179_AddExistingOrgsFromProviderPortal]: FALSE, [FeatureFlag.RecoveryCodeLogin]: FALSE, [FeatureFlag.PM12276_BreadcrumbEventLogs]: FALSE, + [FeatureFlag.PM18794_ProviderPaymentMethod]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
{{ subscription.accountCredit | currency: "$" }}
{{ "creditAppliedDesc" | i18n }}
+ {{ "noPaymentMethod" | i18n }} +
+ + {{ subscription.paymentSource.description }} + - {{ "unverified" | i18n }} +
{{ "taxInformationDesc" | i18n }}