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:
@@ -2,7 +2,7 @@
|
||||
<ng-container bitDialogContent>
|
||||
<app-payment
|
||||
[showAccountCredit]="false"
|
||||
[showBankAccount]="!!organizationId"
|
||||
[showBankAccount]="!!organizationId || !!providerId"
|
||||
[initialPaymentMethod]="initialPaymentMethod"
|
||||
></app-payment>
|
||||
<app-manage-tax-information
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user