mirror of
https://github.com/bitwarden/browser
synced 2026-02-06 19:53:59 +00:00
refactoring the implementation
This commit is contained in:
@@ -34,8 +34,8 @@ import {
|
||||
AdjustPaymentDialogResultType,
|
||||
} from "../../shared/adjust-payment-dialog/adjust-payment-dialog.component";
|
||||
import {
|
||||
TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE,
|
||||
TrialPaymentMethodDialogComponent,
|
||||
TrialPaymentMethodDialogResultType,
|
||||
} from "../../shared/trial-subscription-dialog/trial-payment-method-dialog.component";
|
||||
import { FreeTrial } from "../../types/free-trial";
|
||||
|
||||
@@ -202,7 +202,7 @@ export class OrganizationPaymentMethodComponent implements OnDestroy {
|
||||
},
|
||||
});
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
if (result === TrialPaymentMethodDialogResultType.Submitted) {
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { PlanInterval, ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
|
||||
|
||||
@Injectable()
|
||||
export class PlanSelectionService {
|
||||
getPlanCardContainerClasses(
|
||||
plan: PlanResponse,
|
||||
index: number,
|
||||
isCardDisabled: (index: number) => boolean,
|
||||
): string[] {
|
||||
const isSelected = plan.isAnnual;
|
||||
const isDisabled = isCardDisabled(index);
|
||||
|
||||
if (isDisabled) {
|
||||
return [
|
||||
"tw-cursor-not-allowed",
|
||||
"tw-bg-secondary-100",
|
||||
"tw-font-normal",
|
||||
"tw-bg-blur",
|
||||
"tw-text-muted",
|
||||
"tw-block",
|
||||
"tw-rounded",
|
||||
];
|
||||
}
|
||||
|
||||
return isSelected
|
||||
? [
|
||||
"tw-cursor-pointer",
|
||||
"tw-block",
|
||||
"tw-rounded",
|
||||
"tw-border",
|
||||
"tw-border-solid",
|
||||
"tw-border-primary-600",
|
||||
"hover:tw-border-primary-700",
|
||||
"focus:tw-border-2",
|
||||
"focus:tw-border-primary-700",
|
||||
"focus:tw-rounded-lg",
|
||||
]
|
||||
: [
|
||||
"tw-cursor-pointer",
|
||||
"tw-block",
|
||||
"tw-rounded",
|
||||
"tw-border",
|
||||
"tw-border-solid",
|
||||
"tw-border-secondary-300",
|
||||
"hover:tw-border-text-main",
|
||||
"focus:tw-border-2",
|
||||
"focus:tw-border-primary-700",
|
||||
];
|
||||
}
|
||||
|
||||
selectPlan(
|
||||
plan: PlanResponse,
|
||||
selectedInterval: number,
|
||||
currentPlan: PlanResponse,
|
||||
onPlanSelected: (selectedPlan: PlanResponse) => void,
|
||||
): void {
|
||||
if (
|
||||
selectedInterval === PlanInterval.Monthly &&
|
||||
plan.productTier === ProductTierType.Families
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
onPlanSelected(plan);
|
||||
}
|
||||
|
||||
getSelectablePlans(
|
||||
passwordManagerPlans: PlanResponse[],
|
||||
selectedPlan: PlanResponse,
|
||||
planIsEnabled: (plan: PlanResponse) => boolean,
|
||||
): PlanResponse[] {
|
||||
const result =
|
||||
passwordManagerPlans?.filter(
|
||||
(plan) => plan.productTier === selectedPlan.productTier && planIsEnabled(plan),
|
||||
) || [];
|
||||
|
||||
result.sort((planA, planB) => planA.displaySortOrder - planB.displaySortOrder).reverse();
|
||||
return result;
|
||||
}
|
||||
|
||||
handleKeydown(
|
||||
event: KeyboardEvent,
|
||||
index: number,
|
||||
isCardDisabled: (index: number) => boolean,
|
||||
): void {
|
||||
const cardElements = Array.from(document.querySelectorAll(".product-card")) as HTMLElement[];
|
||||
let newIndex = index;
|
||||
const direction = event.key === "ArrowRight" || event.key === "ArrowDown" ? 1 : -1;
|
||||
|
||||
if (["ArrowRight", "ArrowDown", "ArrowLeft", "ArrowUp"].includes(event.key)) {
|
||||
do {
|
||||
newIndex = (newIndex + direction + cardElements.length) % cardElements.length;
|
||||
} while (isCardDisabled(newIndex) && newIndex !== index);
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
setTimeout(() => {
|
||||
const card = cardElements[newIndex];
|
||||
if (
|
||||
!(
|
||||
card.classList.contains("tw-bg-secondary-100") &&
|
||||
card.classList.contains("tw-text-muted")
|
||||
)
|
||||
) {
|
||||
card?.focus();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
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 { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
|
||||
|
||||
@Injectable()
|
||||
export class PricingCalculationService {
|
||||
calculatePasswordManagerSubtotal(
|
||||
selectedPlan: PlanResponse,
|
||||
subscription: OrganizationSubscriptionResponse,
|
||||
discount: number,
|
||||
): number {
|
||||
if (!selectedPlan || !selectedPlan.PasswordManager) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let subTotal = selectedPlan.PasswordManager.basePrice;
|
||||
if (selectedPlan.PasswordManager.hasAdditionalSeatsOption) {
|
||||
subTotal += this.calculatePasswordManagerSeatTotal(selectedPlan, subscription, false);
|
||||
}
|
||||
if (selectedPlan.PasswordManager.hasPremiumAccessOption) {
|
||||
subTotal += selectedPlan.PasswordManager.premiumAccessOptionPrice;
|
||||
}
|
||||
return subTotal - discount;
|
||||
}
|
||||
|
||||
calculateSecretsManagerSubtotal(
|
||||
selectedPlan: PlanResponse,
|
||||
subscription: OrganizationSubscriptionResponse,
|
||||
secretsManagerTotal: number,
|
||||
): number {
|
||||
const plan = selectedPlan;
|
||||
if (!plan || !plan.SecretsManager) {
|
||||
return secretsManagerTotal || 0;
|
||||
}
|
||||
|
||||
if (secretsManagerTotal) {
|
||||
return secretsManagerTotal;
|
||||
}
|
||||
|
||||
return (
|
||||
plan.SecretsManager.basePrice +
|
||||
this.calculateSecretsManagerSeatTotal(plan, subscription?.smSeats) +
|
||||
this.calculateAdditionalServiceAccountTotal(plan, 0)
|
||||
); // This will be calculated separately
|
||||
}
|
||||
|
||||
getPasswordManagerSeats(
|
||||
selectedPlan: PlanResponse,
|
||||
subscription: OrganizationSubscriptionResponse,
|
||||
): number {
|
||||
if (!selectedPlan) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (selectedPlan.productTier === ProductTierType.Families) {
|
||||
return selectedPlan.PasswordManager.baseSeats;
|
||||
}
|
||||
return subscription?.seats;
|
||||
}
|
||||
|
||||
calculateTotal(
|
||||
organization: Organization,
|
||||
selectedPlan: PlanResponse,
|
||||
passwordManagerSubtotal: number,
|
||||
estimatedTax: number,
|
||||
subscription: OrganizationSubscriptionResponse,
|
||||
): number {
|
||||
if (!organization || !selectedPlan) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (organization.useSecretsManager) {
|
||||
return (
|
||||
this.calculateAdditionalStorageTotal(selectedPlan, subscription) +
|
||||
this.calculateSecretsManagerSubtotal(selectedPlan, subscription, 0) +
|
||||
estimatedTax
|
||||
);
|
||||
}
|
||||
return (
|
||||
passwordManagerSubtotal +
|
||||
this.calculateAdditionalStorageTotal(selectedPlan, subscription) +
|
||||
estimatedTax
|
||||
);
|
||||
}
|
||||
|
||||
calculateAdditionalServiceAccount(
|
||||
currentPlan: PlanResponse,
|
||||
subscription: OrganizationSubscriptionResponse,
|
||||
): number {
|
||||
if (!currentPlan || !currentPlan.SecretsManager) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const baseServiceAccount = currentPlan.SecretsManager?.baseServiceAccount || 0;
|
||||
const usedServiceAccounts = subscription?.smServiceAccounts || 0;
|
||||
|
||||
const additionalServiceAccounts = baseServiceAccount - usedServiceAccounts;
|
||||
|
||||
return additionalServiceAccounts <= 0 ? Math.abs(additionalServiceAccounts) : 0;
|
||||
}
|
||||
|
||||
calculatePasswordManagerSeatTotal(
|
||||
plan: PlanResponse,
|
||||
subscription: OrganizationSubscriptionResponse,
|
||||
isSecretsManagerTrial: boolean,
|
||||
): number {
|
||||
if (!plan.PasswordManager.hasAdditionalSeatsOption || isSecretsManagerTrial) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return plan.PasswordManager.seatPrice * Math.abs(subscription?.seats || 0);
|
||||
}
|
||||
|
||||
calculateSecretsManagerSeatTotal(plan: PlanResponse, seats: number): number {
|
||||
if (!plan.SecretsManager.hasAdditionalSeatsOption) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return plan.SecretsManager.seatPrice * Math.abs(seats || 0);
|
||||
}
|
||||
|
||||
calculateAdditionalStorageTotal(
|
||||
plan: PlanResponse,
|
||||
subscription: OrganizationSubscriptionResponse,
|
||||
): number {
|
||||
if (!plan.PasswordManager.hasAdditionalStorageOption) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (
|
||||
plan.PasswordManager.additionalStoragePricePerGb *
|
||||
Math.abs(subscription?.maxStorageGb ? subscription.maxStorageGb - 1 : 0)
|
||||
);
|
||||
}
|
||||
|
||||
calculateAdditionalServiceAccountTotal(
|
||||
plan: PlanResponse,
|
||||
additionalServiceAccount: number,
|
||||
): number {
|
||||
if (!plan.SecretsManager.hasAdditionalServiceAccountOption || additionalServiceAccount === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return plan.SecretsManager.additionalPricePerServiceAccount * additionalServiceAccount;
|
||||
}
|
||||
|
||||
calculateTotalAppliedDiscount(total: number, discountPercentageFromSub: number): number {
|
||||
return total * (discountPercentageFromSub / 100);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,149 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
|
||||
import { 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 { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.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 { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
|
||||
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { PaymentComponent } from "../payment/payment.component";
|
||||
|
||||
@Injectable()
|
||||
export class TrialPaymentMethodService {
|
||||
isSecretsManagerTrial(subscription: OrganizationSubscriptionResponse): boolean {
|
||||
return (
|
||||
subscription?.subscription?.items?.some((item) =>
|
||||
subscription?.customerDiscount?.appliesTo?.includes(item.productId),
|
||||
) ?? false
|
||||
);
|
||||
}
|
||||
|
||||
shouldShowTaxIdField(
|
||||
organizationId: string,
|
||||
productTier: ProductTierType,
|
||||
providerId?: string,
|
||||
): boolean {
|
||||
if (organizationId) {
|
||||
switch (productTier) {
|
||||
case ProductTierType.Free:
|
||||
case ProductTierType.Families:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return !!providerId;
|
||||
}
|
||||
}
|
||||
|
||||
async submitPayment(
|
||||
organizationId: string,
|
||||
paymentComponent: PaymentComponent,
|
||||
taxInformation: TaxInformation,
|
||||
billingApiService: BillingApiServiceAbstraction,
|
||||
apiService: ApiService,
|
||||
): Promise<void> {
|
||||
if (organizationId) {
|
||||
await this.updateOrganizationPaymentMethod(
|
||||
organizationId,
|
||||
paymentComponent,
|
||||
taxInformation,
|
||||
billingApiService,
|
||||
);
|
||||
} else {
|
||||
await this.updatePremiumUserPaymentMethod(paymentComponent, taxInformation, apiService);
|
||||
}
|
||||
}
|
||||
|
||||
async refreshSalesTax(
|
||||
organizationId: string,
|
||||
selectedPlan: PlanResponse,
|
||||
subscription: OrganizationSubscriptionResponse,
|
||||
organization: Organization,
|
||||
taxInformation: TaxInformation,
|
||||
taxService: TaxServiceAbstraction,
|
||||
i18nService: I18nService,
|
||||
toastService: ToastService,
|
||||
): Promise<number> {
|
||||
const request: PreviewOrganizationInvoiceRequest = {
|
||||
organizationId: organizationId,
|
||||
passwordManager: {
|
||||
additionalStorage: 0,
|
||||
plan: selectedPlan?.type,
|
||||
seats: subscription.seats,
|
||||
},
|
||||
taxInformation: {
|
||||
postalCode: taxInformation.postalCode,
|
||||
country: taxInformation.country,
|
||||
taxId: taxInformation.taxId,
|
||||
},
|
||||
};
|
||||
|
||||
if (organization.useSecretsManager) {
|
||||
request.secretsManager = {
|
||||
seats: subscription.smSeats,
|
||||
additionalMachineAccounts:
|
||||
subscription.smServiceAccounts - subscription.plan.SecretsManager.baseServiceAccount,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const invoice = await taxService.previewOrganizationInvoice(request);
|
||||
return invoice.taxAmount;
|
||||
} catch (error) {
|
||||
const translatedMessage = i18nService.t(error.message);
|
||||
toastService.showToast({
|
||||
title: "",
|
||||
variant: "error",
|
||||
message: !translatedMessage || translatedMessage === "" ? error.message : translatedMessage,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async updateOrganizationPaymentMethod(
|
||||
organizationId: string,
|
||||
paymentComponent: PaymentComponent,
|
||||
taxInformation: TaxInformation,
|
||||
billingApiService: BillingApiServiceAbstraction,
|
||||
): Promise<void> {
|
||||
const paymentSource = await paymentComponent.tokenize();
|
||||
|
||||
const request = new UpdatePaymentMethodRequest();
|
||||
request.paymentSource = paymentSource;
|
||||
request.taxInformation = ExpandedTaxInfoUpdateRequest.From(taxInformation);
|
||||
|
||||
await billingApiService.updateOrganizationPaymentMethod(organizationId, request);
|
||||
}
|
||||
|
||||
private async updatePremiumUserPaymentMethod(
|
||||
paymentComponent: PaymentComponent,
|
||||
taxInformation: TaxInformation,
|
||||
apiService: ApiService,
|
||||
): Promise<void> {
|
||||
const { type, token } = await paymentComponent.tokenize();
|
||||
|
||||
const request = new PaymentRequest();
|
||||
request.paymentMethodType = type;
|
||||
request.paymentToken = token;
|
||||
request.country = taxInformation.country;
|
||||
request.postalCode = taxInformation.postalCode;
|
||||
request.taxId = taxInformation.taxId;
|
||||
request.state = taxInformation.state;
|
||||
request.line1 = taxInformation.line1;
|
||||
request.line2 = taxInformation.line2;
|
||||
request.city = taxInformation.city;
|
||||
request.state = taxInformation.state;
|
||||
|
||||
await apiService.postAccountPayment(request);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user