+ @switch (paymentMethod.type) {
+ @case ("bankAccount") {
+
+ {{ paymentMethod.bankName }}, *{{ paymentMethod.last4 }}
+ @if (paymentMethod.hostedVerificationUrl) {
+ - {{ "unverified" | i18n }}
+ }
+
+ {{ "changePaymentMethod" | i18n }}
+
+ }
+ @case ("card") {
+
+ @let cardBrandIcon = getCardBrandIcon();
+ @if (cardBrandIcon !== null) {
+
+ } @else {
+
+ }
+ {{ paymentMethod.brand | titlecase }}, *{{ paymentMethod.last4 }},
+ {{ paymentMethod.expiration }}
+
+ {{ "changePaymentMethod" | i18n }}
+
+
+ }
+ @case ("payPal") {
+
diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts
index 6fc2dc57ba2..2b5c27e0f09 100644
--- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts
+++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts
@@ -12,9 +12,9 @@ import {
} from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { Router } from "@angular/router";
-import { firstValueFrom, map, Subject, switchMap, takeUntil } from "rxjs";
+import { combineLatest, firstValueFrom, map, Subject, switchMap, takeUntil } from "rxjs";
+import { debounceTime } from "rxjs/operators";
-import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import {
@@ -28,28 +28,8 @@ import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/
import { OrganizationUpgradeRequest } from "@bitwarden/common/admin-console/models/request/organization-upgrade.request";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
-import {
- BillingApiServiceAbstraction,
- BillingInformation,
- OrganizationBillingServiceAbstraction as OrganizationBillingService,
- OrganizationInformation,
- PaymentInformation,
- PlanInformation,
-} from "@bitwarden/common/billing/abstractions";
-import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
-import {
- PaymentMethodType,
- PlanInterval,
- PlanType,
- ProductTierType,
-} 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 { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request";
-import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request";
-import { BillingResponse } from "@bitwarden/common/billing/models/response/billing.response";
+import { PlanInterval, PlanType, ProductTierType } from "@bitwarden/common/billing/enums";
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
-import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response";
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -57,6 +37,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { OrganizationId } from "@bitwarden/common/types/guid";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import {
+ CardComponent,
DIALOG_DATA,
DialogConfig,
DialogRef,
@@ -64,11 +45,25 @@ import {
ToastService,
} from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
-import { UserId } from "@bitwarden/user-core";
+import {
+ OrganizationSubscriptionPlan,
+ SubscriberBillingClient,
+ TaxClient,
+} from "@bitwarden/web-vault/app/billing/clients";
+import {
+ EnterBillingAddressComponent,
+ EnterPaymentMethodComponent,
+ getBillingAddressFromForm,
+} from "@bitwarden/web-vault/app/billing/payment/components";
+import {
+ BillingAddress,
+ getCardBrandIcon,
+ MaskedPaymentMethod,
+} from "@bitwarden/web-vault/app/billing/payment/types";
+import { BitwardenSubscriber } from "@bitwarden/web-vault/app/billing/types";
import { BillingNotificationService } from "../services/billing-notification.service";
import { BillingSharedModule } from "../shared/billing-shared.module";
-import { PaymentComponent } from "../shared/payment/payment.component";
type ChangePlanDialogParams = {
organizationId: string;
@@ -111,11 +106,16 @@ interface OnSuccessArgs {
@Component({
templateUrl: "./change-plan-dialog.component.html",
- imports: [BillingSharedModule],
+ imports: [
+ BillingSharedModule,
+ EnterPaymentMethodComponent,
+ EnterBillingAddressComponent,
+ CardComponent,
+ ],
+ providers: [SubscriberBillingClient, TaxClient],
})
export class ChangePlanDialogComponent implements OnInit, OnDestroy {
- @ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
- @ViewChild(ManageTaxInformationComponent) taxComponent: ManageTaxInformationComponent;
+ @ViewChild(EnterPaymentMethodComponent) enterPaymentMethodComponent: EnterPaymentMethodComponent;
@Input() acceptingSponsorship = false;
@Input() organizationId: string;
@@ -172,7 +172,11 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
clientOwnerEmail: ["", [Validators.email]],
plan: [this.plan],
productTier: [this.productTier],
- // planInterval: [1],
+ });
+
+ billingFormGroup = this.formBuilder.group({
+ paymentMethod: EnterPaymentMethodComponent.getFormGroup(),
+ billingAddress: EnterBillingAddressComponent.getFormGroup(),
});
planType: string;
@@ -183,7 +187,6 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
secretsManagerPlans: PlanResponse[];
organization: Organization;
sub: OrganizationSubscriptionResponse;
- billing: BillingResponse;
dialogHeaderName: string;
currentPlanName: string;
showPayment: boolean = false;
@@ -191,15 +194,14 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
currentPlan: PlanResponse;
isCardStateDisabled = false;
focusedIndex: number | null = null;
- accountCredit: number;
- paymentSource?: PaymentSourceResponse;
plans: ListResponse;
isSubscriptionCanceled: boolean = false;
secretsManagerTotal: number;
- private destroy$ = new Subject();
+ paymentMethod: MaskedPaymentMethod | null;
+ billingAddress: BillingAddress | null;
- protected taxInformation: TaxInformation;
+ private destroy$ = new Subject();
constructor(
@Inject(DIALOG_DATA) private dialogParams: ChangePlanDialogParams,
@@ -215,11 +217,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
private messagingService: MessagingService,
private formBuilder: FormBuilder,
private organizationApiService: OrganizationApiServiceAbstraction,
- private billingApiService: BillingApiServiceAbstraction,
- private taxService: TaxServiceAbstraction,
private accountService: AccountService,
- private organizationBillingService: OrganizationBillingService,
private billingNotificationService: BillingNotificationService,
+ private subscriberBillingClient: SubscriberBillingClient,
+ private taxClient: TaxClient,
) {}
async ngOnInit(): Promise {
@@ -242,10 +243,14 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
);
if (this.sub?.subscription?.status !== "canceled") {
try {
- const { accountCredit, paymentSource } =
- await this.billingApiService.getOrganizationPaymentMethod(this.organizationId);
- this.accountCredit = accountCredit;
- this.paymentSource = paymentSource;
+ const subscriber: BitwardenSubscriber = { type: "organization", data: this.organization };
+ const [paymentMethod, billingAddress] = await Promise.all([
+ this.subscriberBillingClient.getPaymentMethod(subscriber),
+ this.subscriberBillingClient.getBillingAddress(subscriber),
+ ]);
+
+ this.paymentMethod = paymentMethod;
+ this.billingAddress = billingAddress;
} catch (error) {
this.billingNotificationService.handleError(error);
}
@@ -307,15 +312,24 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
? 0
: (this.sub?.customerDiscount?.percentOff ?? 0);
- this.setInitialPlanSelection();
- this.loading = false;
-
- const taxInfo = await this.organizationApiService.getTaxInfo(this.organizationId);
- this.taxInformation = TaxInformation.from(taxInfo);
-
+ await this.setInitialPlanSelection();
if (!this.isSubscriptionCanceled) {
- this.refreshSalesTax();
+ await this.refreshSalesTax();
}
+
+ combineLatest([
+ this.billingFormGroup.controls.billingAddress.controls.country.valueChanges,
+ this.billingFormGroup.controls.billingAddress.controls.postalCode.valueChanges,
+ this.billingFormGroup.controls.billingAddress.controls.taxId.valueChanges,
+ ])
+ .pipe(
+ debounceTime(1000),
+ switchMap(async () => await this.refreshSalesTax()),
+ takeUntil(this.destroy$),
+ )
+ .subscribe();
+
+ this.loading = false;
}
resolveHeaderName(subscription: OrganizationSubscriptionResponse): string {
@@ -333,10 +347,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
);
}
- setInitialPlanSelection() {
+ async setInitialPlanSelection() {
this.focusedIndex = this.selectableProducts.length - 1;
if (!this.isSubscriptionCanceled) {
- this.selectPlan(this.getPlanByType(ProductTierType.Enterprise));
+ await this.selectPlan(this.getPlanByType(ProductTierType.Enterprise));
}
}
@@ -344,10 +358,6 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
return this.selectableProducts.find((product) => product.productTier === productTier);
}
- isPaymentSourceEmpty() {
- return this.paymentSource === null || this.paymentSource === undefined;
- }
-
isSecretsManagerTrial(): boolean {
return (
this.sub?.subscription?.items?.some((item) =>
@@ -356,13 +366,13 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
);
}
- planTypeChanged() {
- this.selectPlan(this.getPlanByType(ProductTierType.Enterprise));
+ async planTypeChanged() {
+ await this.selectPlan(this.getPlanByType(ProductTierType.Enterprise));
}
- updateInterval(event: number) {
+ async updateInterval(event: number) {
this.selectedInterval = event;
- this.planTypeChanged();
+ await this.planTypeChanged();
}
protected getPlanIntervals() {
@@ -460,7 +470,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
}
}
- protected selectPlan(plan: PlanResponse) {
+ protected async selectPlan(plan: PlanResponse) {
if (
this.selectedInterval === PlanInterval.Monthly &&
plan.productTier == ProductTierType.Families
@@ -475,7 +485,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
this.formGroup.patchValue({ productTier: plan.productTier });
try {
- this.refreshSalesTax();
+ await this.refreshSalesTax();
} catch {
this.estimatedTax = 0;
}
@@ -489,19 +499,11 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
get upgradeRequiresPaymentMethod() {
const isFreeTier = this.organization?.productTierType === ProductTierType.Free;
const shouldHideFree = !this.showFree;
- const hasNoPaymentSource = !this.paymentSource;
+ const hasNoPaymentSource = !this.paymentMethod;
return isFreeTier && shouldHideFree && hasNoPaymentSource;
}
- get selectedSecretsManagerPlan() {
- let planResponse: PlanResponse;
- if (this.secretsManagerPlans) {
- return this.secretsManagerPlans.find((plan) => plan.type === this.selectedPlan.type);
- }
- return planResponse;
- }
-
get selectedPlanInterval() {
if (this.isSubscriptionCanceled) {
return this.currentPlan.isAnnual ? "year" : "month";
@@ -591,8 +593,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
return 0;
}
- const result = plan.PasswordManager.seatPrice * Math.abs(this.sub?.seats || 0);
- return result;
+ return plan.PasswordManager.seatPrice * Math.abs(this.sub?.seats || 0);
}
secretsManagerSeatTotal(plan: PlanResponse, seats: number): number {
@@ -746,39 +747,22 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
this.formGroup.controls.additionalSeats.setValue(1);
}
- changedCountry() {
- this.paymentComponent.showBankAccount = this.taxInformation.country === "US";
-
- if (
- !this.paymentComponent.showBankAccount &&
- this.paymentComponent.selected === PaymentMethodType.BankAccount
- ) {
- this.paymentComponent.select(PaymentMethodType.Card);
- }
- }
-
- protected taxInformationChanged(event: TaxInformation): void {
- this.taxInformation = event;
- this.changedCountry();
- this.refreshSalesTax();
- }
-
submit = async () => {
- if (this.taxComponent !== undefined && !this.taxComponent.validate()) {
- this.taxComponent.markAllAsTouched();
+ this.formGroup.markAllAsTouched();
+ this.billingFormGroup.markAllAsTouched();
+ if (this.formGroup.invalid || (this.billingFormGroup.invalid && !this.paymentMethod)) {
return;
}
const doSubmit = async (): Promise => {
- const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
- let orgId: string = null;
+ let orgId: string;
const sub = this.sub?.subscription;
const isCanceled = sub?.status === "canceled";
const isCancelledDowngradedToFreeOrg =
sub?.cancelled && this.organization.productTierType === ProductTierType.Free;
if (isCanceled || isCancelledDowngradedToFreeOrg) {
- await this.restartSubscription(activeUserId);
+ await this.restartSubscription();
orgId = this.organizationId;
} else {
orgId = await this.updateOrganization();
@@ -795,9 +779,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
await this.syncService.fullSync(true);
if (!this.acceptingSponsorship && !this.isInTrialFlow) {
- // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- this.router.navigate(["/organizations/" + orgId + "/billing/subscription"]);
+ await this.router.navigate(["/organizations/" + orgId + "/billing/subscription"]);
}
if (this.isInTrialFlow) {
@@ -818,46 +800,13 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
this.dialogRef.close();
};
- private async restartSubscription(activeUserId: UserId) {
- const org = await this.organizationApiService.get(this.organizationId);
- const organization: OrganizationInformation = {
- name: org.name,
- billingEmail: org.billingEmail,
- };
-
- const filteredPlan = this.plans.data
- .filter((plan) => plan.productTier === this.selectedPlan.productTier && !plan.legacyYear)
- .find((plan) => {
- const isSameBillingCycle = plan.isAnnual === this.selectedPlan.isAnnual;
- return isSameBillingCycle;
- });
-
- const plan: PlanInformation = {
- type: filteredPlan.type,
- passwordManagerSeats: org.seats,
- };
-
- if (org.useSecretsManager) {
- plan.subscribeToSecretsManager = true;
- plan.secretsManagerSeats = org.smSeats;
- }
-
- const { type, token } = await this.paymentComponent.tokenize();
- const paymentMethod: [string, PaymentMethodType] = [token, type];
-
- const payment: PaymentInformation = {
+ private async restartSubscription() {
+ const paymentMethod = await this.enterPaymentMethodComponent.tokenize();
+ const billingAddress = getBillingAddressFromForm(this.billingFormGroup.controls.billingAddress);
+ await this.subscriberBillingClient.restartSubscription(
+ { type: "organization", data: this.organization },
paymentMethod,
- billing: this.getBillingInformationFromTaxInfoComponent(),
- };
-
- await this.organizationBillingService.restartSubscription(
- this.organization.id,
- {
- organization,
- plan,
- payment,
- },
- activeUserId,
+ billingAddress,
);
}
@@ -875,25 +824,25 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
this.formGroup.controls.premiumAccessAddon.value;
request.planType = this.selectedPlan.type;
if (this.showPayment) {
- request.billingAddressCountry = this.taxInformation.country;
- request.billingAddressPostalCode = this.taxInformation.postalCode;
+ request.billingAddressCountry = this.billingFormGroup.controls.billingAddress.value.country;
+ request.billingAddressPostalCode =
+ this.billingFormGroup.controls.billingAddress.value.postalCode;
}
// Secrets Manager
this.buildSecretsManagerRequest(request);
- if (this.upgradeRequiresPaymentMethod || this.showPayment || this.isPaymentSourceEmpty()) {
- const tokenizedPaymentSource = await this.paymentComponent.tokenize();
- const updatePaymentMethodRequest = new UpdatePaymentMethodRequest();
- updatePaymentMethodRequest.paymentSource = tokenizedPaymentSource;
- updatePaymentMethodRequest.taxInformation = ExpandedTaxInfoUpdateRequest.From(
- this.taxInformation,
+ if (this.upgradeRequiresPaymentMethod || this.showPayment || !this.paymentMethod) {
+ const paymentMethod = await this.enterPaymentMethodComponent.tokenize();
+ const billingAddress = getBillingAddressFromForm(
+ this.billingFormGroup.controls.billingAddress,
);
- await this.billingApiService.updateOrganizationPaymentMethod(
- this.organizationId,
- updatePaymentMethodRequest,
- );
+ const subscriber: BitwardenSubscriber = { type: "organization", data: this.organization };
+ await Promise.all([
+ this.subscriberBillingClient.updatePaymentMethod(subscriber, paymentMethod, null),
+ this.subscriberBillingClient.updateBillingAddress(subscriber, billingAddress),
+ ]);
}
// Backfill pub/priv key if necessary
@@ -931,18 +880,6 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
return text;
}
- private getBillingInformationFromTaxInfoComponent(): BillingInformation {
- return {
- country: this.taxInformation.country,
- postalCode: this.taxInformation.postalCode,
- taxId: this.taxInformation.taxId,
- addressLine1: this.taxInformation.line1,
- addressLine2: this.taxInformation.line2,
- city: this.taxInformation.city,
- state: this.taxInformation.state,
- };
- }
-
private buildSecretsManagerRequest(request: OrganizationUpgradeRequest): void {
request.useSecretsManager = this.organization.useSecretsManager;
if (!this.organization.useSecretsManager) {
@@ -1002,25 +939,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
}
calculateTotalAppliedDiscount(total: number) {
- const discountedTotal = total * (this.discountPercentageFromSub / 100);
- return discountedTotal;
- }
-
- get paymentSourceClasses() {
- if (this.paymentSource == null) {
- return [];
- }
- switch (this.paymentSource.type) {
- case PaymentMethodType.Card:
- return ["bwi-credit-card"];
- case PaymentMethodType.BankAccount:
- case PaymentMethodType.Check:
- return ["bwi-billing"];
- case PaymentMethodType.PayPal:
- return ["bwi-paypal text-primary"];
- default:
- return [];
- }
+ return total * (this.discountPercentageFromSub / 100);
}
resolvePlanName(productTier: ProductTierType) {
@@ -1064,9 +983,9 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
}
}
- onFocus(index: number) {
+ async onFocus(index: number) {
this.focusedIndex = index;
- this.selectPlan(this.selectableProducts[index]);
+ await this.selectPlan(this.selectableProducts[index]);
}
isCardDisabled(index: number): boolean {
@@ -1078,58 +997,44 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
return index;
}
- private refreshSalesTax(): void {
- if (
- this.taxInformation === undefined ||
- !this.taxInformation.country ||
- !this.taxInformation.postalCode
- ) {
+ private async refreshSalesTax(): Promise {
+ if (this.billingFormGroup.controls.billingAddress.invalid && !this.billingAddress) {
return;
}
- const request: PreviewOrganizationInvoiceRequest = {
- organizationId: this.organizationId,
- passwordManager: {
- additionalStorage: 0,
- plan: this.selectedPlan?.type,
- seats: this.sub.seats,
- },
- taxInformation: {
- postalCode: this.taxInformation.postalCode,
- country: this.taxInformation.country,
- taxId: this.taxInformation.taxId,
- },
+ const getPlanFromLegacyEnum = (planType: PlanType): OrganizationSubscriptionPlan => {
+ switch (planType) {
+ case PlanType.FamiliesAnnually:
+ return { tier: "families", cadence: "annually" };
+ case PlanType.TeamsMonthly:
+ return { tier: "teams", cadence: "monthly" };
+ case PlanType.TeamsAnnually:
+ return { tier: "teams", cadence: "annually" };
+ case PlanType.EnterpriseMonthly:
+ return { tier: "enterprise", cadence: "monthly" };
+ case PlanType.EnterpriseAnnually:
+ return { tier: "enterprise", cadence: "annually" };
+ }
};
- if (this.organization.useSecretsManager) {
- request.secretsManager = {
- seats: this.sub.smSeats,
- additionalMachineAccounts:
- this.sub.smServiceAccounts - this.sub.plan.SecretsManager.baseServiceAccount,
- };
- }
+ const billingAddress = this.billingFormGroup.controls.billingAddress.valid
+ ? getBillingAddressFromForm(this.billingFormGroup.controls.billingAddress)
+ : this.billingAddress;
- this.taxService
- .previewOrganizationInvoice(request)
- .then((invoice) => {
- this.estimatedTax = invoice.taxAmount;
- })
- .catch((error) => {
- const translatedMessage = this.i18nService.t(error.message);
- this.toastService.showToast({
- title: "",
- variant: "error",
- message:
- !translatedMessage || translatedMessage === "" ? error.message : translatedMessage,
- });
- });
+ const taxAmounts = await this.taxClient.previewTaxForOrganizationSubscriptionPlanChange(
+ this.organizationId,
+ getPlanFromLegacyEnum(this.selectedPlan.type),
+ billingAddress,
+ );
+
+ this.estimatedTax = taxAmounts.tax;
}
protected canUpdatePaymentInformation(): boolean {
return (
this.upgradeRequiresPaymentMethod ||
this.showPayment ||
- this.isPaymentSourceEmpty() ||
+ !this.paymentMethod ||
this.isSubscriptionCanceled
);
}
@@ -1146,4 +1051,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
return this.i18nService.t("upgrade");
}
}
+
+ get supportsTaxId() {
+ return this.formGroup.value.productTier !== ProductTierType.Families;
+ }
+
+ getCardBrandIcon = () => getCardBrandIcon(this.paymentMethod);
}
diff --git a/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts b/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts
index 692791db855..5c8df483587 100644
--- a/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts
+++ b/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts
@@ -11,7 +11,6 @@ import { WebPlatformUtilsService } from "../../core/web-platform-utils.service";
import { OrgBillingHistoryViewComponent } from "./organization-billing-history-view.component";
import { OrganizationSubscriptionCloudComponent } from "./organization-subscription-cloud.component";
import { OrganizationSubscriptionSelfhostComponent } from "./organization-subscription-selfhost.component";
-import { OrganizationPaymentMethodComponent } from "./payment-method/organization-payment-method.component";
const routes: Routes = [
{
@@ -26,17 +25,6 @@ const routes: Routes = [
: OrganizationSubscriptionCloudComponent,
data: { titleId: "subscription" },
},
- {
- path: "payment-method",
- component: OrganizationPaymentMethodComponent,
- canActivate: [
- organizationPermissionsGuard((org) => org.canEditPaymentMethods),
- organizationIsUnmanaged,
- ],
- data: {
- titleId: "paymentMethod",
- },
- },
{
path: "payment-details",
component: OrganizationPaymentDetailsComponent,
diff --git a/apps/web/src/app/billing/organizations/organization-billing.module.ts b/apps/web/src/app/billing/organizations/organization-billing.module.ts
index 707a854de02..90ba04c4fa4 100644
--- a/apps/web/src/app/billing/organizations/organization-billing.module.ts
+++ b/apps/web/src/app/billing/organizations/organization-billing.module.ts
@@ -17,7 +17,6 @@ import { OrganizationBillingRoutingModule } from "./organization-billing-routing
import { OrganizationPlansComponent } from "./organization-plans.component";
import { OrganizationSubscriptionCloudComponent } from "./organization-subscription-cloud.component";
import { OrganizationSubscriptionSelfhostComponent } from "./organization-subscription-selfhost.component";
-import { OrganizationPaymentMethodComponent } from "./payment-method/organization-payment-method.component";
import { SecretsManagerAdjustSubscriptionComponent } from "./sm-adjust-subscription.component";
import { SecretsManagerSubscribeStandaloneComponent } from "./sm-subscribe-standalone.component";
import { SubscriptionHiddenComponent } from "./subscription-hidden.component";
@@ -45,7 +44,6 @@ import { SubscriptionStatusComponent } from "./subscription-status.component";
SecretsManagerSubscribeStandaloneComponent,
SubscriptionHiddenComponent,
SubscriptionStatusComponent,
- OrganizationPaymentMethodComponent,
],
})
export class OrganizationBillingModule {}
diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.html b/apps/web/src/app/billing/organizations/organization-plans.component.html
index 3b765927c3c..6234fc6e6e3 100644
--- a/apps/web/src/app/billing/organizations/organization-plans.component.html
+++ b/apps/web/src/app/billing/organizations/organization-plans.component.html
@@ -404,17 +404,16 @@
{{ paymentDesc }}
-
-
-
+
+ }
+
+ >
+
{{ "passwordManagerPlanPrice" | i18n }}: {{ passwordManagerSubtotal | currency: "USD $" }}
diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.ts b/apps/web/src/app/billing/organizations/organization-plans.component.ts
index 820bee950eb..cbeedc454dc 100644
--- a/apps/web/src/app/billing/organizations/organization-plans.component.ts
+++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts
@@ -11,10 +11,9 @@ import {
} from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { Router } from "@angular/router";
-import { Subject, firstValueFrom, takeUntil } from "rxjs";
+import { firstValueFrom, merge, Subject, takeUntil } from "rxjs";
import { debounceTime, map, switchMap } from "rxjs/operators";
-import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import {
@@ -32,24 +31,12 @@ import { ProviderOrganizationCreateRequest } from "@bitwarden/common/admin-conso
import { ProviderResponse } from "@bitwarden/common/admin-console/models/response/provider/provider.response";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
-import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
-import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
-import {
- PaymentMethodType,
- PlanSponsorshipType,
- PlanType,
- ProductTierType,
-} 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 { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request";
-import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request";
+import { PlanSponsorshipType, PlanType, ProductTierType } from "@bitwarden/common/billing/enums";
import { BillingResponse } from "@bitwarden/common/billing/models/response/billing.response";
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
-import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -59,10 +46,20 @@ import { OrgKey } from "@bitwarden/common/types/key";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { ToastService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
+import {
+ OrganizationSubscriptionPlan,
+ SubscriberBillingClient,
+ TaxClient,
+} from "@bitwarden/web-vault/app/billing/clients";
+import {
+ EnterBillingAddressComponent,
+ EnterPaymentMethodComponent,
+ getBillingAddressFromForm,
+} from "@bitwarden/web-vault/app/billing/payment/components";
+import { tokenizablePaymentMethodToLegacyEnum } from "@bitwarden/web-vault/app/billing/payment/types";
import { OrganizationCreateModule } from "../../admin-console/organizations/create/organization-create.module";
import { BillingSharedModule, secretsManagerSubscribeFormFactory } from "../shared";
-import { PaymentComponent } from "../shared/payment/payment.component";
interface OnSuccessArgs {
organizationId: string;
@@ -78,11 +75,16 @@ const Allowed2020PlansForLegacyProviders = [
@Component({
selector: "app-organization-plans",
templateUrl: "organization-plans.component.html",
- imports: [BillingSharedModule, OrganizationCreateModule],
+ imports: [
+ BillingSharedModule,
+ OrganizationCreateModule,
+ EnterPaymentMethodComponent,
+ EnterBillingAddressComponent,
+ ],
+ providers: [SubscriberBillingClient, TaxClient],
})
export class OrganizationPlansComponent implements OnInit, OnDestroy {
- @ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
- @ViewChild(ManageTaxInformationComponent) taxComponent: ManageTaxInformationComponent;
+ @ViewChild(EnterPaymentMethodComponent) enterPaymentMethodComponent!: EnterPaymentMethodComponent;
@Input() organizationId?: string;
@Input() showFree = true;
@@ -105,8 +107,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
private _productTier = ProductTierType.Free;
- protected taxInformation: TaxInformation;
-
@Input()
get plan(): PlanType {
return this._plan;
@@ -135,10 +135,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
secretsManagerSubscription = secretsManagerSubscribeFormFactory(this.formBuilder);
- selfHostedForm = this.formBuilder.group({
- file: [null, [Validators.required]],
- });
-
formGroup = this.formBuilder.group({
name: [""],
billingEmail: ["", [Validators.email]],
@@ -152,6 +148,11 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
secretsManager: this.secretsManagerSubscription,
});
+ billingFormGroup = this.formBuilder.group({
+ paymentMethod: EnterPaymentMethodComponent.getFormGroup(),
+ billingAddress: EnterBillingAddressComponent.getFormGroup(),
+ });
+
passwordManagerPlans: PlanResponse[];
secretsManagerPlans: PlanResponse[];
organization: Organization;
@@ -179,10 +180,9 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
private organizationApiService: OrganizationApiServiceAbstraction,
private providerApiService: ProviderApiServiceAbstraction,
private toastService: ToastService,
- private configService: ConfigService,
- private billingApiService: BillingApiServiceAbstraction,
- private taxService: TaxServiceAbstraction,
private accountService: AccountService,
+ private subscriberBillingClient: SubscriberBillingClient,
+ private taxClient: TaxClient,
) {
this.selfHosted = this.platformUtilsService.isSelfHost();
}
@@ -199,9 +199,14 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
);
this.billing = await this.organizationApiService.getBilling(this.organizationId);
this.sub = await this.organizationApiService.getSubscription(this.organizationId);
- this.taxInformation = await this.organizationApiService.getTaxInfo(this.organizationId);
- } else if (!this.selfHosted) {
- this.taxInformation = await this.apiService.getTaxInfo();
+ const billingAddress = await this.subscriberBillingClient.getBillingAddress({
+ type: "organization",
+ data: this.organization,
+ });
+ this.billingFormGroup.controls.billingAddress.patchValue({
+ ...billingAddress,
+ taxId: billingAddress?.taxId?.value,
+ });
}
if (!this.selfHosted) {
@@ -268,15 +273,17 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
this.loading = false;
- this.formGroup.valueChanges.pipe(debounceTime(1000), takeUntil(this.destroy$)).subscribe(() => {
- this.refreshSalesTax();
- });
-
- this.secretsManagerForm.valueChanges
- .pipe(debounceTime(1000), takeUntil(this.destroy$))
- .subscribe(() => {
- this.refreshSalesTax();
- });
+ merge(
+ this.formGroup.valueChanges,
+ this.billingFormGroup.valueChanges,
+ this.secretsManagerForm.valueChanges,
+ )
+ .pipe(
+ debounceTime(1000),
+ switchMap(async () => await this.refreshSalesTax()),
+ takeUntil(this.destroy$),
+ )
+ .subscribe();
if (this.enableSecretsManagerByDefault && this.selectedSecretsManagerPlan) {
this.secretsManagerSubscription.patchValue({
@@ -587,34 +594,13 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
this.changedProduct();
}
- protected changedCountry(): void {
- this.paymentComponent.showBankAccount = this.taxInformation?.country === "US";
- if (
- !this.paymentComponent.showBankAccount &&
- this.paymentComponent.selected === PaymentMethodType.BankAccount
- ) {
- this.paymentComponent.select(PaymentMethodType.Card);
- }
- }
-
- protected onTaxInformationChanged(event: TaxInformation): void {
- this.taxInformation = event;
- this.changedCountry();
- this.refreshSalesTax();
- }
-
protected cancel(): void {
this.onCanceled.emit();
}
- protected setSelectedFile(event: Event): void {
- const fileInputEl =
event.target;
- this.selectedFile = fileInputEl.files.length > 0 ? fileInputEl.files[0] : null;
- }
-
submit = async () => {
- if (this.taxComponent && !this.taxComponent.validate()) {
- this.taxComponent.markAllAsTouched();
+ this.formGroup.markAllAsTouched();
+ if (this.formGroup.invalid) {
return;
}
@@ -688,46 +674,54 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
}
}
- private refreshSalesTax(): void {
- if (!this.taxComponent.validate()) {
+ private async refreshSalesTax(): Promise {
+ if (this.billingFormGroup.controls.billingAddress.invalid) {
return;
}
- const request: PreviewOrganizationInvoiceRequest = {
- organizationId: this.organizationId,
- passwordManager: {
- additionalStorage: this.formGroup.controls.additionalStorage.value,
- plan: this.formGroup.controls.plan.value,
- sponsoredPlan: this.planSponsorshipType,
- seats: this.formGroup.controls.additionalSeats.value,
- },
- taxInformation: {
- postalCode: this.taxInformation.postalCode,
- country: this.taxInformation.country,
- taxId: this.taxInformation.taxId,
- },
+ const getPlanFromLegacyEnum = (): OrganizationSubscriptionPlan => {
+ switch (this.formGroup.value.plan) {
+ case PlanType.FamiliesAnnually:
+ return { tier: "families", cadence: "annually" };
+ case PlanType.TeamsMonthly:
+ return { tier: "teams", cadence: "monthly" };
+ case PlanType.TeamsAnnually:
+ return { tier: "teams", cadence: "annually" };
+ case PlanType.EnterpriseMonthly:
+ return { tier: "enterprise", cadence: "monthly" };
+ case PlanType.EnterpriseAnnually:
+ return { tier: "enterprise", cadence: "annually" };
+ }
};
- if (this.secretsManagerForm.controls.enabled.value === true) {
- request.secretsManager = {
- seats: this.secretsManagerForm.controls.userSeats.value,
- additionalMachineAccounts: this.secretsManagerForm.controls.additionalServiceAccounts.value,
- };
- }
+ const billingAddress = getBillingAddressFromForm(this.billingFormGroup.controls.billingAddress);
- this.taxService
- .previewOrganizationInvoice(request)
- .then((invoice) => {
- this.estimatedTax = invoice.taxAmount;
- this.total = invoice.totalAmount;
- })
- .catch((error) => {
- this.toastService.showToast({
- title: "",
- variant: "error",
- message: this.i18nService.t(error.message),
- });
- });
+ const passwordManagerSeats =
+ this.formGroup.value.productTier === ProductTierType.Families
+ ? 1
+ : this.formGroup.value.additionalSeats;
+
+ const taxAmounts = await this.taxClient.previewTaxForOrganizationSubscriptionPurchase(
+ {
+ ...getPlanFromLegacyEnum(),
+ passwordManager: {
+ seats: passwordManagerSeats,
+ additionalStorage: this.formGroup.value.additionalStorage,
+ sponsored: false,
+ },
+ secretsManager: this.formGroup.value.secretsManager.enabled
+ ? {
+ seats: this.secretsManagerForm.value.userSeats,
+ additionalServiceAccounts: this.secretsManagerForm.value.additionalServiceAccounts,
+ standalone: false,
+ }
+ : undefined,
+ },
+ billingAddress,
+ );
+
+ this.estimatedTax = taxAmounts.tax;
+ this.total = taxAmounts.total;
}
private async updateOrganization() {
@@ -738,21 +732,24 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
this.selectedPlan.PasswordManager.hasPremiumAccessOption &&
this.formGroup.controls.premiumAccessAddon.value;
request.planType = this.selectedPlan.type;
- request.billingAddressCountry = this.taxInformation?.country;
- request.billingAddressPostalCode = this.taxInformation?.postalCode;
+ request.billingAddressCountry = this.billingFormGroup.value.billingAddress.country;
+ request.billingAddressPostalCode = this.billingFormGroup.value.billingAddress.postalCode;
// Secrets Manager
this.buildSecretsManagerRequest(request);
if (this.upgradeRequiresPaymentMethod) {
- const updatePaymentMethodRequest = new UpdatePaymentMethodRequest();
- updatePaymentMethodRequest.paymentSource = await this.paymentComponent.tokenize();
- updatePaymentMethodRequest.taxInformation = ExpandedTaxInfoUpdateRequest.From(
- this.taxInformation,
- );
- await this.billingApiService.updateOrganizationPaymentMethod(
- this.organizationId,
- updatePaymentMethodRequest,
+ if (this.billingFormGroup.invalid) {
+ return;
+ }
+ const paymentMethod = await this.enterPaymentMethodComponent.tokenize();
+ await this.subscriberBillingClient.updatePaymentMethod(
+ { type: "organization", data: this.organization },
+ paymentMethod,
+ {
+ country: this.billingFormGroup.value.billingAddress.country,
+ postalCode: this.billingFormGroup.value.billingAddress.postalCode,
+ },
);
}
@@ -791,23 +788,31 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
if (this.selectedPlan.type === PlanType.Free) {
request.planType = PlanType.Free;
} else {
- const { type, token } = await this.paymentComponent.tokenize();
+ if (this.billingFormGroup.invalid) {
+ return;
+ }
- request.paymentToken = token;
- request.paymentMethodType = type;
+ const paymentMethod = await this.enterPaymentMethodComponent.tokenize();
+
+ const billingAddress = getBillingAddressFromForm(
+ this.billingFormGroup.controls.billingAddress,
+ );
+
+ request.paymentToken = paymentMethod.token;
+ request.paymentMethodType = tokenizablePaymentMethodToLegacyEnum(paymentMethod.type);
request.additionalSeats = this.formGroup.controls.additionalSeats.value;
request.additionalStorageGb = this.formGroup.controls.additionalStorage.value;
request.premiumAccessAddon =
this.selectedPlan.PasswordManager.hasPremiumAccessOption &&
this.formGroup.controls.premiumAccessAddon.value;
request.planType = this.selectedPlan.type;
- request.billingAddressPostalCode = this.taxInformation?.postalCode;
- request.billingAddressCountry = this.taxInformation?.country;
- request.taxIdNumber = this.taxInformation?.taxId;
- request.billingAddressLine1 = this.taxInformation?.line1;
- request.billingAddressLine2 = this.taxInformation?.line2;
- request.billingAddressCity = this.taxInformation?.city;
- request.billingAddressState = this.taxInformation?.state;
+ request.billingAddressPostalCode = billingAddress.postalCode;
+ request.billingAddressCountry = billingAddress.country;
+ request.taxIdNumber = billingAddress.taxId?.value;
+ request.billingAddressLine1 = billingAddress.line1;
+ request.billingAddressLine2 = billingAddress.line2;
+ request.billingAddressCity = billingAddress.city;
+ request.billingAddressState = billingAddress.state;
}
// Secrets Manager
diff --git a/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.ts b/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.ts
index 47742ba0a88..b2bf27e726a 100644
--- a/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.ts
+++ b/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.ts
@@ -1,15 +1,11 @@
import { Component, OnDestroy, OnInit } from "@angular/core";
-import { ActivatedRoute, Router } from "@angular/router";
+import { ActivatedRoute } from "@angular/router";
import {
BehaviorSubject,
- catchError,
combineLatest,
- EMPTY,
filter,
firstValueFrom,
- from,
lastValueFrom,
- map,
merge,
Observable,
of,
@@ -22,15 +18,13 @@ import {
withLatestFrom,
} from "rxjs";
-import {
- getOrganizationById,
- OrganizationService,
-} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
+import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
+import { getById } from "@bitwarden/common/platform/misc";
import { DialogService } from "@bitwarden/components";
import { CommandDefinition, MessageListener } from "@bitwarden/messaging";
import { SubscriberBillingClient } from "@bitwarden/web-vault/app/billing/clients";
@@ -54,13 +48,6 @@ import { TaxIdWarningType } from "@bitwarden/web-vault/app/billing/warnings/type
import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module";
import { SharedModule } from "@bitwarden/web-vault/app/shared";
-class RedirectError {
- constructor(
- public path: string[],
- public relativeTo: ActivatedRoute,
- ) {}
-}
-
type View = {
organization: BitwardenSubscriber;
paymentMethod: MaskedPaymentMethod | null;
@@ -93,24 +80,12 @@ export class OrganizationPaymentDetailsComponent implements OnInit, OnDestroy {
switchMap((userId) =>
this.organizationService
.organizations$(userId)
- .pipe(getOrganizationById(this.activatedRoute.snapshot.params.organizationId)),
+ .pipe(getById(this.activatedRoute.snapshot.params.organizationId)),
),
filter((organization): organization is Organization => !!organization),
);
private load$: Observable = this.organization$.pipe(
- switchMap((organization) =>
- this.configService
- .getFeatureFlag$(FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout)
- .pipe(
- map((managePaymentDetailsOutsideCheckout) => {
- if (!managePaymentDetailsOutsideCheckout) {
- throw new RedirectError(["../payment-method"], this.activatedRoute);
- }
- return organization;
- }),
- ),
- ),
mapOrganizationToSubscriber,
switchMap(async (organization) => {
const getTaxIdWarning = firstValueFrom(
@@ -132,14 +107,6 @@ export class OrganizationPaymentDetailsComponent implements OnInit, OnDestroy {
taxIdWarning,
};
}),
- catchError((error: unknown) => {
- if (error instanceof RedirectError) {
- return from(this.router.navigate(error.path, { relativeTo: error.relativeTo })).pipe(
- switchMap(() => EMPTY),
- );
- }
- throw error;
- }),
);
view$: Observable = merge(
@@ -159,7 +126,6 @@ export class OrganizationPaymentDetailsComponent implements OnInit, OnDestroy {
private messageListener: MessageListener,
private organizationService: OrganizationService,
private organizationWarningsService: OrganizationWarningsService,
- private router: Router,
private subscriberBillingClient: SubscriberBillingClient,
) {}
diff --git a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.html b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.html
deleted file mode 100644
index ab31147e916..00000000000
--- a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.html
+++ /dev/null
@@ -1,48 +0,0 @@
-
-
-
-
- {{ "loading" | i18n }}
-
-
-
-
-
- {{ accountCreditHeaderText }}
-
- {{ Math.abs(accountCredit) | currency: "$" }}
- {{ "creditAppliedDesc" | i18n }}
-
-
-
-
- {{ "paymentMethod" | i18n }}
- {{ "noPaymentMethod" | i18n }}
-
-
-
-
-
- {{ paymentSource.description }}
- - {{ "unverified" | i18n }}
-
-
-
-
- {{ "paymentChargedWithUnpaidSubscription" | i18n }}
-
-
-
-
diff --git a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts
deleted file mode 100644
index 4106ee4f9cd..00000000000
--- a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts
+++ /dev/null
@@ -1,288 +0,0 @@
-import { Location } from "@angular/common";
-import { Component, OnDestroy } from "@angular/core";
-import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
-import { ActivatedRoute, Router } from "@angular/router";
-import { combineLatest, firstValueFrom, from, lastValueFrom, map, switchMap } from "rxjs";
-
-import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
-import {
- OrganizationService,
- getOrganizationById,
-} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
-import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
-import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
-import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
-import { PaymentMethodType } from "@bitwarden/common/billing/enums";
-import { TaxInformation } from "@bitwarden/common/billing/models/domain";
-import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request";
-import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
-import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
-import { SyncService } from "@bitwarden/common/platform/sync";
-import { DialogService, ToastService } from "@bitwarden/components";
-
-import { BillingNotificationService } from "../../services/billing-notification.service";
-import {
- AddCreditDialogResult,
- openAddCreditDialog,
-} from "../../shared/add-credit-dialog.component";
-import {
- AdjustPaymentDialogComponent,
- AdjustPaymentDialogResultType,
-} from "../../shared/adjust-payment-dialog/adjust-payment-dialog.component";
-import {
- TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE,
- TrialPaymentDialogComponent,
-} from "../../shared/trial-payment-dialog/trial-payment-dialog.component";
-
-@Component({
- templateUrl: "./organization-payment-method.component.html",
- standalone: false,
-})
-export class OrganizationPaymentMethodComponent implements OnDestroy {
- organizationId!: string;
- isUnpaid = false;
- accountCredit?: number;
- paymentSource?: PaymentSourceResponse;
- subscriptionStatus?: string;
- organization?: Organization;
- organizationSubscriptionResponse?: OrganizationSubscriptionResponse;
-
- loading = true;
-
- protected readonly Math = Math;
- launchPaymentModalAutomatically = false;
-
- protected taxInformation?: TaxInformation;
-
- constructor(
- private activatedRoute: ActivatedRoute,
- private billingApiService: BillingApiServiceAbstraction,
- protected organizationApiService: OrganizationApiServiceAbstraction,
- private dialogService: DialogService,
- private i18nService: I18nService,
- private platformUtilsService: PlatformUtilsService,
- private router: Router,
- private toastService: ToastService,
- private location: Location,
- private organizationService: OrganizationService,
- private accountService: AccountService,
- protected syncService: SyncService,
- private billingNotificationService: BillingNotificationService,
- private configService: ConfigService,
- ) {
- combineLatest([
- this.activatedRoute.params,
- this.configService.getFeatureFlag$(FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout),
- ])
- .pipe(
- switchMap(([{ organizationId }, managePaymentDetailsOutsideCheckout]) => {
- if (this.platformUtilsService.isSelfHost()) {
- return from(this.router.navigate(["/settings/subscription"]));
- }
-
- if (managePaymentDetailsOutsideCheckout) {
- return from(
- this.router.navigate(["../payment-details"], { relativeTo: this.activatedRoute }),
- );
- }
-
- this.organizationId = organizationId;
- return from(this.load());
- }),
- takeUntilDestroyed(),
- )
- .subscribe();
-
- const state = this.router.getCurrentNavigation()?.extras?.state;
- // In case the above state is undefined or null, we use redundantState
- const redundantState: any = location.getState();
- const queryParam = this.activatedRoute.snapshot.queryParamMap.get(
- "launchPaymentModalAutomatically",
- );
- if (state && Object.prototype.hasOwnProperty.call(state, "launchPaymentModalAutomatically")) {
- this.launchPaymentModalAutomatically = state.launchPaymentModalAutomatically;
- } else if (
- redundantState &&
- Object.prototype.hasOwnProperty.call(redundantState, "launchPaymentModalAutomatically")
- ) {
- this.launchPaymentModalAutomatically = redundantState.launchPaymentModalAutomatically;
- } else {
- this.launchPaymentModalAutomatically = queryParam === "true";
- }
- }
- ngOnDestroy(): void {
- this.launchPaymentModalAutomatically = false;
- }
-
- protected addAccountCredit = async (): Promise => {
- if (this.subscriptionStatus === "trialing") {
- const hasValidBillingAddress = await this.checkBillingAddressForTrialingOrg();
- if (!hasValidBillingAddress) {
- return;
- }
- }
- const dialogRef = openAddCreditDialog(this.dialogService, {
- data: {
- organizationId: this.organizationId,
- },
- });
-
- const result = await lastValueFrom(dialogRef.closed);
-
- if (result === AddCreditDialogResult.Added) {
- await this.load();
- }
- };
-
- protected load = async (): Promise => {
- this.loading = true;
- try {
- const { accountCredit, paymentSource, subscriptionStatus, taxInformation } =
- await this.billingApiService.getOrganizationPaymentMethod(this.organizationId);
- this.accountCredit = accountCredit;
- this.paymentSource = paymentSource;
- this.subscriptionStatus = subscriptionStatus;
- this.taxInformation = taxInformation;
- this.isUnpaid = this.subscriptionStatus === "unpaid";
-
- if (this.organizationId) {
- const organizationSubscriptionPromise = this.organizationApiService.getSubscription(
- this.organizationId,
- );
-
- const userId = await firstValueFrom(
- this.accountService.activeAccount$.pipe(map((a) => a?.id)),
- );
-
- if (!userId) {
- throw new Error("User ID is not found");
- }
-
- const organizationPromise = await firstValueFrom(
- this.organizationService
- .organizations$(userId)
- .pipe(getOrganizationById(this.organizationId)),
- );
-
- [this.organizationSubscriptionResponse, this.organization] = await Promise.all([
- organizationSubscriptionPromise,
- organizationPromise,
- ]);
-
- if (!this.organization) {
- throw new Error("Organization is not found");
- }
- if (!this.paymentSource) {
- throw new Error("Payment source is not found");
- }
- }
- // If the flag `launchPaymentModalAutomatically` is set to true,
- // we schedule a timeout (delay of 800ms) to automatically launch the payment modal.
- // This delay ensures that any prior UI/rendering operations complete before triggering the modal.
- if (this.launchPaymentModalAutomatically) {
- window.setTimeout(async () => {
- await this.changePayment();
- this.launchPaymentModalAutomatically = false;
- this.location.replaceState(this.location.path(), "", {});
- }, 800);
- }
- } catch (error) {
- this.billingNotificationService.handleError(error);
- } finally {
- this.loading = false;
- }
- };
-
- protected updatePaymentMethod = async (): Promise => {
- const dialogRef = AdjustPaymentDialogComponent.open(this.dialogService, {
- data: {
- initialPaymentMethod: this.paymentSource?.type,
- organizationId: this.organizationId,
- productTier: this.organization?.productTierType,
- },
- });
-
- const result = await lastValueFrom(dialogRef.closed);
-
- if (result === AdjustPaymentDialogResultType.Submitted) {
- await this.load();
- }
- };
-
- changePayment = async () => {
- const dialogRef = TrialPaymentDialogComponent.open(this.dialogService, {
- data: {
- organizationId: this.organizationId,
- subscription: this.organizationSubscriptionResponse!,
- productTierType: this.organization!.productTierType,
- },
- });
- const result = await lastValueFrom(dialogRef.closed);
- if (result === TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE.SUBMITTED) {
- this.location.replaceState(this.location.path(), "", {});
- if (this.launchPaymentModalAutomatically && !this.organization?.enabled) {
- await this.syncService.fullSync(true);
- }
- this.launchPaymentModalAutomatically = false;
- await this.load();
- }
- };
-
- protected verifyBankAccount = async (request: VerifyBankAccountRequest): Promise => {
- await this.billingApiService.verifyOrganizationBankAccount(this.organizationId, request);
- this.toastService.showToast({
- variant: "success",
- title: "",
- message: this.i18nService.t("verifiedBankAccount"),
- });
- };
-
- protected get accountCreditHeaderText(): string {
- const hasAccountCredit = this.accountCredit && this.accountCredit > 0;
- const key = hasAccountCredit ? "accountCredit" : "accountBalance";
- return this.i18nService.t(key);
- }
-
- protected get paymentSourceClasses() {
- if (this.paymentSource == null) {
- return [];
- }
- switch (this.paymentSource.type) {
- case PaymentMethodType.Card:
- return ["bwi-credit-card"];
- case PaymentMethodType.BankAccount:
- case PaymentMethodType.Check:
- return ["bwi-billing"];
- case PaymentMethodType.PayPal:
- return ["bwi-paypal text-primary"];
- default:
- return [];
- }
- }
-
- protected get subscriptionIsUnpaid(): boolean {
- return this.subscriptionStatus === "unpaid";
- }
-
- protected get updatePaymentSourceButtonText(): string {
- const key = this.paymentSource == null ? "addPaymentMethod" : "changePaymentMethod";
- return this.i18nService.t(key);
- }
-
- private async checkBillingAddressForTrialingOrg(): Promise {
- const hasBillingAddress = this.taxInformation != null;
- if (!hasBillingAddress) {
- this.toastService.showToast({
- variant: "error",
- title: "",
- message: this.i18nService.t("billingAddressRequiredToAddCredit"),
- });
- return false;
- }
- return true;
- }
-}
diff --git a/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.spec.ts b/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.spec.ts
index c7a297cc28b..53f72558089 100644
--- a/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.spec.ts
+++ b/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.spec.ts
@@ -15,8 +15,6 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-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 { DialogRef, DialogService } from "@bitwarden/components";
import { OrganizationBillingClient } from "@bitwarden/web-vault/app/billing/clients";
@@ -35,7 +33,6 @@ import { TaxIdWarningTypes } from "@bitwarden/web-vault/app/billing/warnings/typ
describe("OrganizationWarningsService", () => {
let service: OrganizationWarningsService;
- let configService: MockProxy;
let dialogService: MockProxy;
let i18nService: MockProxy;
let organizationApiService: MockProxy;
@@ -57,7 +54,6 @@ describe("OrganizationWarningsService", () => {
});
beforeEach(() => {
- configService = mock();
dialogService = mock();
i18nService = mock();
organizationApiService = mock();
@@ -94,7 +90,6 @@ describe("OrganizationWarningsService", () => {
TestBed.configureTestingModule({
providers: [
OrganizationWarningsService,
- { provide: ConfigService, useValue: configService },
{ provide: DialogService, useValue: dialogService },
{ provide: I18nService, useValue: i18nService },
{ provide: OrganizationApiServiceAbstraction, useValue: organizationApiService },
@@ -466,7 +461,6 @@ describe("OrganizationWarningsService", () => {
} as OrganizationWarningsResponse);
dialogService.openSimpleDialog.mockResolvedValue(true);
- configService.getFeatureFlag.mockResolvedValue(false);
router.navigate.mockResolvedValue(true);
service.showInactiveSubscriptionDialog$(organization).subscribe({
@@ -478,11 +472,8 @@ describe("OrganizationWarningsService", () => {
acceptButtonText: "Continue",
cancelButtonText: "Close",
});
- expect(configService.getFeatureFlag).toHaveBeenCalledWith(
- FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout,
- );
expect(router.navigate).toHaveBeenCalledWith(
- ["organizations", "org-id-123", "billing", "payment-method"],
+ ["organizations", "org-id-123", "billing", "payment-details"],
{ state: { launchPaymentModalAutomatically: true } },
);
done();
@@ -497,7 +488,6 @@ describe("OrganizationWarningsService", () => {
} as OrganizationWarningsResponse);
dialogService.openSimpleDialog.mockResolvedValue(true);
- configService.getFeatureFlag.mockResolvedValue(true);
router.navigate.mockResolvedValue(true);
service.showInactiveSubscriptionDialog$(organization).subscribe({
@@ -522,7 +512,6 @@ describe("OrganizationWarningsService", () => {
service.showInactiveSubscriptionDialog$(organization).subscribe({
complete: () => {
expect(dialogService.openSimpleDialog).toHaveBeenCalled();
- expect(configService.getFeatureFlag).not.toHaveBeenCalled();
expect(router.navigate).not.toHaveBeenCalled();
done();
},
diff --git a/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.ts b/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.ts
index c6bb1bc231b..46a34def28b 100644
--- a/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.ts
+++ b/apps/web/src/app/billing/organizations/warnings/services/organization-warnings.service.ts
@@ -16,8 +16,6 @@ import { take } from "rxjs/operators";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
-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 { OrganizationId } from "@bitwarden/common/types/guid";
import { DialogService } from "@bitwarden/components";
@@ -53,7 +51,6 @@ export class OrganizationWarningsService {
taxIdWarningRefreshed$ = this.taxIdWarningRefreshedSubject.asObservable();
constructor(
- private configService: ConfigService,
private dialogService: DialogService,
private i18nService: I18nService,
private organizationApiService: OrganizationApiServiceAbstraction,
@@ -196,14 +193,8 @@ export class OrganizationWarningsService {
cancelButtonText: this.i18nService.t("close"),
});
if (confirmed) {
- const managePaymentDetailsOutsideCheckout = await this.configService.getFeatureFlag(
- FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout,
- );
- const route = managePaymentDetailsOutsideCheckout
- ? "payment-details"
- : "payment-method";
await this.router.navigate(
- ["organizations", `${organization.id}`, "billing", route],
+ ["organizations", `${organization.id}`, "billing", "payment-details"],
{
state: { launchPaymentModalAutomatically: true },
},
diff --git a/apps/web/src/app/billing/payment/components/display-payment-method.component.ts b/apps/web/src/app/billing/payment/components/display-payment-method.component.ts
index c33d805aed7..5f5e3442935 100644
--- a/apps/web/src/app/billing/payment/components/display-payment-method.component.ts
+++ b/apps/web/src/app/billing/payment/components/display-payment-method.component.ts
@@ -5,7 +5,7 @@ import { DialogService } from "@bitwarden/components";
import { SharedModule } from "../../../shared";
import { BitwardenSubscriber } from "../../types";
-import { MaskedPaymentMethod } from "../types";
+import { getCardBrandIcon, MaskedPaymentMethod } from "../types";
import { ChangePaymentMethodDialogComponent } from "./change-payment-method-dialog.component";
@@ -40,9 +40,9 @@ import { ChangePaymentMethodDialogComponent } from "./change-payment-method-dial
}
@case ("card") {
- @let brandIcon = getBrandIconForCard();
- @if (brandIcon !== null) {
-
+ @let cardBrandIcon = getCardBrandIcon();
+ @if (cardBrandIcon !== null) {
+
} @else {
}
@@ -74,16 +74,6 @@ export class DisplayPaymentMethodComponent {
@Input({ required: true }) paymentMethod!: MaskedPaymentMethod | null;
@Output() updated = new EventEmitter();
- protected availableCardIcons: Record = {
- amex: "card-amex",
- diners: "card-diners-club",
- discover: "card-discover",
- jcb: "card-jcb",
- mastercard: "card-mastercard",
- unionpay: "card-unionpay",
- visa: "card-visa",
- };
-
constructor(private dialogService: DialogService) {}
changePaymentMethod = async (): Promise => {
@@ -100,13 +90,5 @@ export class DisplayPaymentMethodComponent {
}
};
- protected getBrandIconForCard = (): string | null => {
- if (this.paymentMethod?.type !== "card") {
- return null;
- }
-
- return this.paymentMethod.brand in this.availableCardIcons
- ? this.availableCardIcons[this.paymentMethod.brand]
- : null;
- };
+ protected getCardBrandIcon = () => getCardBrandIcon(this.paymentMethod);
}
diff --git a/apps/web/src/app/billing/payment/components/edit-billing-address-dialog.component.ts b/apps/web/src/app/billing/payment/components/edit-billing-address-dialog.component.ts
index de2f2f94497..6e356097d32 100644
--- a/apps/web/src/app/billing/payment/components/edit-billing-address-dialog.component.ts
+++ b/apps/web/src/app/billing/payment/components/edit-billing-address-dialog.component.ts
@@ -11,10 +11,7 @@ import {
ToastService,
} from "@bitwarden/components";
import { SubscriberBillingClient } from "@bitwarden/web-vault/app/billing/clients";
-import {
- BillingAddress,
- getTaxIdTypeForCountry,
-} from "@bitwarden/web-vault/app/billing/payment/types";
+import { BillingAddress } from "@bitwarden/web-vault/app/billing/payment/types";
import { BitwardenSubscriber } from "@bitwarden/web-vault/app/billing/types";
import {
TaxIdWarningType,
@@ -22,7 +19,10 @@ import {
} from "@bitwarden/web-vault/app/billing/warnings/types";
import { SharedModule } from "@bitwarden/web-vault/app/shared";
-import { EnterBillingAddressComponent } from "./enter-billing-address.component";
+import {
+ EnterBillingAddressComponent,
+ getBillingAddressFromForm,
+} from "./enter-billing-address.component";
type DialogParams = {
subscriber: BitwardenSubscriber;
@@ -104,13 +104,7 @@ export class EditBillingAddressDialogComponent {
return;
}
- const { taxId, ...addressFields } = this.formGroup.getRawValue();
-
- const taxIdType = taxId ? getTaxIdTypeForCountry(addressFields.country) : null;
-
- const billingAddress = taxIdType
- ? { ...addressFields, taxId: { code: taxIdType.code, value: taxId! } }
- : { ...addressFields, taxId: null };
+ const billingAddress = getBillingAddressFromForm(this.formGroup);
const result = await this.billingClient.updateBillingAddress(
this.dialogParams.subscriber,
diff --git a/apps/web/src/app/billing/payment/components/enter-billing-address.component.ts b/apps/web/src/app/billing/payment/components/enter-billing-address.component.ts
index 7659b7ed5ca..3f68c12c897 100644
--- a/apps/web/src/app/billing/payment/components/enter-billing-address.component.ts
+++ b/apps/web/src/app/billing/payment/components/enter-billing-address.component.ts
@@ -24,6 +24,17 @@ export interface BillingAddressControls {
export type BillingAddressFormGroup = FormGroup>;
+export const getBillingAddressFromForm = (formGroup: BillingAddressFormGroup): BillingAddress =>
+ getBillingAddressFromControls(formGroup.getRawValue());
+
+export const getBillingAddressFromControls = (controls: BillingAddressControls) => {
+ const { taxId, ...addressFields } = controls;
+ const taxIdType = taxId ? getTaxIdTypeForCountry(addressFields.country) : null;
+ return taxIdType
+ ? { ...addressFields, taxId: { code: taxIdType.code, value: taxId! } }
+ : { ...addressFields, taxId: null };
+};
+
type Scenario =
| {
type: "checkout";
@@ -67,54 +78,56 @@ type Scenario =
/>
-
-
- {{ "address1" | i18n }}
-
-
-
-
-
- {{ "address2" | i18n }}
-
-
-
-
-
- {{ "cityTown" | i18n }}
-
-
-
-
-
- {{ "stateProvince" | i18n }}
-
-
-
+ @if (scenario.type === "update") {
+
+
+ {{ "address1" | i18n }}
+
+
+
+
+
+ {{ "address2" | i18n }}
+
+
+
+
+
+ {{ "cityTown" | i18n }}
+
+
+
+
+
+ {{ "stateProvince" | i18n }}
+
+
+
+ }
@if (supportsTaxId$ | async) {
@@ -175,7 +188,7 @@ export class EnterBillingAddressComponent implements OnInit, OnDestroy {
this.supportsTaxId$ = this.group.controls.country.valueChanges.pipe(
startWith(this.group.value.country ?? this.selectableCountries[0].value),
map((country) => {
- if (!this.scenario.supportsTaxId) {
+ if (!this.scenario.supportsTaxId || country === "US") {
return false;
}
diff --git a/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts b/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts
index 93c45b873fe..4af5226e7ee 100644
--- a/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts
+++ b/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts
@@ -8,7 +8,6 @@ import { PopoverModule, ToastService } from "@bitwarden/components";
import { SharedModule } from "../../../shared";
import { BillingServicesModule, BraintreeService, StripeService } from "../../services";
-import { PaymentLabelComponent } from "../../shared/payment/payment-label.component";
import {
isTokenizablePaymentMethod,
selectableCountries,
@@ -16,6 +15,8 @@ import {
TokenizedPaymentMethod,
} from "../types";
+import { PaymentLabelComponent } from "./payment-label.component";
+
type PaymentMethodOption = TokenizablePaymentMethod | "accountCredit";
type PaymentMethodFormGroup = FormGroup<{
@@ -102,7 +103,7 @@ type PaymentMethodFormGroup = FormGroup<{
{{ "paymentMethod" | i18n }}
-
-
+
+
{{ "submit" | i18n }}
diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts
index 72ca0bc8391..0fa69c7a0e6 100644
--- a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts
+++ b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts
@@ -1,25 +1,24 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom, Subject, switchMap } from "rxjs";
import { first, takeUntil } from "rxjs/operators";
-import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components";
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
import { ProviderSetupRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-setup.request";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
-import { PaymentMethodType } from "@bitwarden/common/billing/enums";
-import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { ProviderKey } from "@bitwarden/common/types/key";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { ToastService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
-import { PaymentComponent } from "@bitwarden/web-vault/app/billing/shared/payment/payment.component";
+import {
+ EnterBillingAddressComponent,
+ EnterPaymentMethodComponent,
+ getBillingAddressFromForm,
+} from "@bitwarden/web-vault/app/billing/payment/components";
@Component({
selector: "provider-setup",
@@ -27,16 +26,17 @@ import { PaymentComponent } from "@bitwarden/web-vault/app/billing/shared/paymen
standalone: false,
})
export class SetupComponent implements OnInit, OnDestroy {
- @ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
- @ViewChild(ManageTaxInformationComponent) taxInformationComponent: ManageTaxInformationComponent;
+ @ViewChild(EnterPaymentMethodComponent) enterPaymentMethodComponent!: EnterPaymentMethodComponent;
loading = true;
- providerId: string;
- token: string;
+ providerId!: string;
+ token!: string;
protected formGroup = this.formBuilder.group({
name: ["", Validators.required],
billingEmail: ["", [Validators.required, Validators.email]],
+ paymentMethod: EnterPaymentMethodComponent.getFormGroup(),
+ billingAddress: EnterBillingAddressComponent.getFormGroup(),
});
private destroy$ = new Subject();
@@ -69,7 +69,7 @@ export class SetupComponent implements OnInit, OnDestroy {
if (error) {
this.toastService.showToast({
variant: "error",
- title: null,
+ title: "",
message: this.i18nService.t("emergencyInviteAcceptFailed"),
timeout: 10000,
});
@@ -95,6 +95,7 @@ export class SetupComponent implements OnInit, OnDestroy {
replaceUrl: true,
});
}
+
this.loading = false;
} catch (error) {
this.validationService.showError(error);
@@ -115,10 +116,7 @@ export class SetupComponent implements OnInit, OnDestroy {
try {
this.formGroup.markAllAsTouched();
- const paymentValid = this.paymentComponent.validate();
- const taxInformationValid = this.taxInformationComponent.validate();
-
- if (!paymentValid || !taxInformationValid || !this.formGroup.valid) {
+ if (this.formGroup.invalid) {
return;
}
const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
@@ -126,29 +124,24 @@ export class SetupComponent implements OnInit, OnDestroy {
const key = providerKey[0].encryptedString;
const request = new ProviderSetupRequest();
- request.name = this.formGroup.value.name;
- request.billingEmail = this.formGroup.value.billingEmail;
+ request.name = this.formGroup.value.name!;
+ request.billingEmail = this.formGroup.value.billingEmail!;
request.token = this.token;
- request.key = key;
+ request.key = key!;
- request.taxInfo = new ExpandedTaxInfoUpdateRequest();
- const taxInformation = this.taxInformationComponent.getTaxInformation();
+ const paymentMethod = await this.enterPaymentMethodComponent.tokenize();
+ if (!paymentMethod) {
+ return;
+ }
- request.taxInfo.country = taxInformation.country;
- request.taxInfo.postalCode = taxInformation.postalCode;
- request.taxInfo.taxId = taxInformation.taxId;
- request.taxInfo.line1 = taxInformation.line1;
- request.taxInfo.line2 = taxInformation.line2;
- request.taxInfo.city = taxInformation.city;
- request.taxInfo.state = taxInformation.state;
-
- request.paymentSource = await this.paymentComponent.tokenize();
+ request.paymentMethod = paymentMethod;
+ request.billingAddress = getBillingAddressFromForm(this.formGroup.controls.billingAddress);
const provider = await this.providerApiService.postProviderSetup(this.providerId, request);
this.toastService.showToast({
variant: "success",
- title: null,
+ title: "",
message: this.i18nService.t("providerSetup"),
});
@@ -156,20 +149,10 @@ export class SetupComponent implements OnInit, OnDestroy {
await this.router.navigate(["/providers", provider.id]);
} catch (e) {
- if (
- this.paymentComponent.selected === PaymentMethodType.PayPal &&
- typeof e === "string" &&
- e === "No payment method is available."
- ) {
- this.toastService.showToast({
- variant: "error",
- title: null,
- message: this.i18nService.t("clickPayWithPayPal"),
- });
- } else {
+ if (e !== null && typeof e === "object" && "message" in e && typeof e.message === "string") {
e.message = this.i18nService.translate(e.message) || e.message;
- this.validationService.showError(e);
}
+ this.validationService.showError(e);
}
};
}
diff --git a/libs/angular/src/billing/components/invoices/invoices.component.html b/bitwarden_license/bit-web/src/app/billing/providers/billing-history/invoices.component.html
similarity index 100%
rename from libs/angular/src/billing/components/invoices/invoices.component.html
rename to bitwarden_license/bit-web/src/app/billing/providers/billing-history/invoices.component.html
diff --git a/libs/angular/src/billing/components/invoices/invoices.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/billing-history/invoices.component.ts
similarity index 100%
rename from libs/angular/src/billing/components/invoices/invoices.component.ts
rename to bitwarden_license/bit-web/src/app/billing/providers/billing-history/invoices.component.ts
diff --git a/libs/angular/src/billing/components/invoices/no-invoices.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/billing-history/no-invoices.component.ts
similarity index 100%
rename from libs/angular/src/billing/components/invoices/no-invoices.component.ts
rename to bitwarden_license/bit-web/src/app/billing/providers/billing-history/no-invoices.component.ts
diff --git a/bitwarden_license/bit-web/src/app/billing/providers/index.ts b/bitwarden_license/bit-web/src/app/billing/providers/index.ts
index b1294bc8047..3cd83e68990 100644
--- a/bitwarden_license/bit-web/src/app/billing/providers/index.ts
+++ b/bitwarden_license/bit-web/src/app/billing/providers/index.ts
@@ -1,3 +1,5 @@
+export * from "./billing-history/invoices.component";
+export * from "./billing-history/no-invoices.component";
export * from "./billing-history/provider-billing-history.component";
export * from "./clients";
export * from "./guards/has-consolidated-billing.guard";
diff --git a/bitwarden_license/bit-web/src/app/billing/providers/payment-details/provider-payment-details.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/payment-details/provider-payment-details.component.ts
index d2ac2cede2f..5a070687de4 100644
--- a/bitwarden_license/bit-web/src/app/billing/providers/payment-details/provider-payment-details.component.ts
+++ b/bitwarden_license/bit-web/src/app/billing/providers/payment-details/provider-payment-details.component.ts
@@ -1,13 +1,10 @@
import { Component, OnDestroy, OnInit } from "@angular/core";
-import { ActivatedRoute, Router } from "@angular/router";
+import { ActivatedRoute } from "@angular/router";
import {
BehaviorSubject,
combineLatest,
- EMPTY,
filter,
firstValueFrom,
- from,
- map,
merge,
Observable,
of,
@@ -19,7 +16,6 @@ import {
tap,
withLatestFrom,
} from "rxjs";
-import { catchError } from "rxjs/operators";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
@@ -49,13 +45,6 @@ import { SharedModule } from "@bitwarden/web-vault/app/shared";
import { ProviderWarningsService } from "../warnings/services";
-class RedirectError {
- constructor(
- public path: string[],
- public relativeTo: ActivatedRoute,
- ) {}
-}
-
type View = {
activeUserId: UserId;
provider: BitwardenSubscriber;
@@ -92,18 +81,6 @@ export class ProviderPaymentDetailsComponent implements OnInit, OnDestroy {
);
private load$: Observable = this.provider$.pipe(
- switchMap((provider) =>
- this.configService
- .getFeatureFlag$(FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout)
- .pipe(
- map((managePaymentDetailsOutsideCheckout) => {
- if (!managePaymentDetailsOutsideCheckout) {
- throw new RedirectError(["../subscription"], this.activatedRoute);
- }
- return provider;
- }),
- ),
- ),
mapProviderToSubscriber,
switchMap(async (provider) => {
const getTaxIdWarning = firstValueFrom(
@@ -131,14 +108,6 @@ export class ProviderPaymentDetailsComponent implements OnInit, OnDestroy {
};
}),
shareReplay({ bufferSize: 1, refCount: false }),
- catchError((error: unknown) => {
- if (error instanceof RedirectError) {
- return from(this.router.navigate(error.path, { relativeTo: error.relativeTo })).pipe(
- switchMap(() => EMPTY),
- );
- }
- throw error;
- }),
);
view$: Observable = merge(
@@ -158,7 +127,6 @@ export class ProviderPaymentDetailsComponent implements OnInit, OnDestroy {
private messageListener: MessageListener,
private providerService: ProviderService,
private providerWarningsService: ProviderWarningsService,
- private router: Router,
private subscriberBillingClient: SubscriberBillingClient,
) {}
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 0205d2838d1..05eda7e7ea4 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
@@ -62,51 +62,5 @@
- @if (!managePaymentDetailsOutsideCheckout) {
-
-