1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-18794] Allow provider payment method (#13825)

* Allow provider payment method

* Run prettier
This commit is contained in:
Alex Morask
2025-03-14 11:33:21 -04:00
committed by GitHub
parent 4d68952ef3
commit 2ecfac40b7
9 changed files with 198 additions and 23 deletions

View File

@@ -2,7 +2,7 @@
<ng-container bitDialogContent>
<app-payment
[showAccountCredit]="false"
[showBankAccount]="!!organizationId"
[showBankAccount]="!!organizationId || !!providerId"
[initialPaymentMethod]="initialPaymentMethod"
></app-payment>
<app-manage-tax-information

View File

@@ -22,6 +22,7 @@ export interface AdjustPaymentDialogParams {
initialPaymentMethod?: PaymentMethodType;
organizationId?: string;
productTier?: ProductTierType;
providerId?: string;
}
export enum AdjustPaymentDialogResultType {
@@ -44,6 +45,7 @@ export class AdjustPaymentDialogComponent implements OnInit {
protected initialPaymentMethod: PaymentMethodType;
protected organizationId?: string;
protected productTier?: ProductTierType;
protected providerId?: string;
protected taxInformation: TaxInformation;
@@ -61,6 +63,7 @@ export class AdjustPaymentDialogComponent implements OnInit {
this.initialPaymentMethod = this.dialogParams.initialPaymentMethod ?? PaymentMethodType.Card;
this.organizationId = this.dialogParams.organizationId;
this.productTier = this.dialogParams.productTier;
this.providerId = this.dialogParams.providerId;
}
ngOnInit(): void {
@@ -73,6 +76,13 @@ export class AdjustPaymentDialogComponent implements OnInit {
.catch(() => {
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<AdjustPaymentDialogParams>,

View File

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

View File

@@ -63,15 +63,40 @@
</div>
</ng-container>
<!-- Account Credit -->
<ng-container>
<bit-section>
<h2 bitTypography="h2">
{{ "accountCredit" | i18n }}
</h2>
<p class="tw-text-lg tw-font-bold">{{ subscription.accountCredit | currency: "$" }}</p>
<p bitTypography="body1">{{ "creditAppliedDesc" | i18n }}</p>
</ng-container>
</bit-section>
<!-- Payment Method -->
<bit-section *ngIf="allowProviderPaymentMethod$ | async">
<h2 bitTypography="h2">{{ "paymentMethod" | i18n }}</h2>
<p *ngIf="!subscription.paymentSource" bitTypography="body1">
{{ "noPaymentMethod" | i18n }}
</p>
<ng-container *ngIf="subscription.paymentSource">
<app-verify-bank-account
*ngIf="subscription.paymentSource.needsVerification"
[onSubmit]="verifyBankAccount"
(submitted)="load()"
>
</app-verify-bank-account>
<p>
<i class="bwi bwi-fw" [ngClass]="paymentSourceClasses"></i>
{{ subscription.paymentSource.description }}
<span *ngIf="subscription.paymentSource.needsVerification"
>- {{ "unverified" | i18n }}</span
>
</p>
</ng-container>
<button type="button" bitButton buttonType="secondary" [bitAction]="updatePaymentMethod">
{{ updatePaymentSourceButtonText }}
</button>
</bit-section>
<!-- Tax Information -->
<ng-container>
<bit-section>
<h2 bitTypography="h2" class="tw-mt-16">{{ "taxInformation" | i18n }}</h2>
<p>{{ "taxInformationDesc" | i18n }}</p>
<app-manage-tax-information
@@ -80,6 +105,6 @@
[onSubmit]="updateTaxInformation"
(taxInformationUpdated)="load()"
/>
</ng-container>
</bit-section>
</ng-container>
</bit-container>

View File

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

View File

@@ -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<ProviderSubscriptionResponse>;
getProviderTaxInformation: (providerId: string) => Promise<TaxInfoResponse>;
updateOrganizationPaymentMethod: (
organizationId: string,
request: UpdatePaymentMethodRequest,
@@ -66,6 +70,11 @@ export abstract class BillingApiServiceAbstraction {
request: UpdateClientOrganizationRequest,
) => Promise<any>;
updateProviderPaymentMethod: (
providerId: string,
request: UpdatePaymentMethodRequest,
) => Promise<void>;
updateProviderTaxInformation: (
providerId: string,
request: ExpandedTaxInfoUpdateRequest,
@@ -76,6 +85,11 @@ export abstract class BillingApiServiceAbstraction {
request: VerifyBankAccountRequest,
) => Promise<void>;
verifyProviderBankAccount: (
providerId: string,
request: VerifyBankAccountRequest,
) => Promise<void>;
restartSubscription: (
organizationId: string,
request: OrganizationCreateRequest,

View File

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

View File

@@ -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<TaxInfoResponse> {
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<void> {
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<void> {
return await this.apiService.send(
"POST",
"/providers/" + providerId + "/billing/payment-method/verify-bank-account",
request,
true,
false,
);
}
async restartSubscription(
organizationId: string,
request: OrganizationCreateRequest,

View File

@@ -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<FeatureFlag, AllowedFeatureFlagTypes>;
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;