mirror of
https://github.com/bitwarden/browser
synced 2026-02-04 02:33:33 +00:00
feat(billing): Add Upgrade Payment Service
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { mock, mockReset } from "jest-mock-extended";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationResponse } from "@bitwarden/common/admin-console/models/response/organization.response";
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
|
||||
import { PaymentMethodType, PlanType } from "@bitwarden/common/billing/enums";
|
||||
import { PreviewInvoiceResponse } from "@bitwarden/common/billing/models/response/preview-invoice.response";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
|
||||
import { SubscriberBillingClient } from "../../../../clients";
|
||||
import { TokenizedPaymentMethod } from "../../../../payment/types";
|
||||
import { BitwardenSubscriber } from "../../../../types";
|
||||
import { PersonalSubscriptionPricingTierIds } from "../../../../types/subscription-pricing-tier";
|
||||
|
||||
import { UpgradePaymentService, PlanDetails } from "./upgrade-payment.service";
|
||||
|
||||
describe("UpgradePaymentService", () => {
|
||||
const mockOrganizationBillingService = mock<OrganizationBillingServiceAbstraction>();
|
||||
const mockSubscriberBillingClient = mock<SubscriberBillingClient>();
|
||||
const mockTaxService = mock<TaxServiceAbstraction>();
|
||||
const mockLogService = mock<LogService>();
|
||||
const mockApiService = mock<ApiService>();
|
||||
const mockSyncService = mock<SyncService>();
|
||||
|
||||
mockApiService.refreshIdentityToken.mockResolvedValue({});
|
||||
mockSyncService.fullSync.mockResolvedValue(true);
|
||||
|
||||
let sut: UpgradePaymentService;
|
||||
|
||||
const mockSubscriber: BitwardenSubscriber = {
|
||||
type: "account",
|
||||
data: {
|
||||
id: "user-id" as UserId,
|
||||
email: "test@example.com",
|
||||
} as Account,
|
||||
};
|
||||
|
||||
const mockTokenizedPaymentMethod: TokenizedPaymentMethod = {
|
||||
token: "test-token",
|
||||
type: "card",
|
||||
};
|
||||
|
||||
const mockBillingAddress = {
|
||||
country: "US",
|
||||
postalCode: "12345",
|
||||
};
|
||||
|
||||
const mockPremiumPlanDetails: PlanDetails = {
|
||||
tier: PersonalSubscriptionPricingTierIds.Premium,
|
||||
details: {
|
||||
id: PersonalSubscriptionPricingTierIds.Premium,
|
||||
name: "Premium",
|
||||
description: "Premium plan",
|
||||
availableCadences: ["annually"],
|
||||
passwordManager: {
|
||||
type: "standalone",
|
||||
annualPrice: 10,
|
||||
annualPricePerAdditionalStorageGB: 4,
|
||||
features: [
|
||||
{ key: "feature1", value: "Feature 1" },
|
||||
{ key: "feature2", value: "Feature 2" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockFamiliesPlanDetails: PlanDetails = {
|
||||
tier: PersonalSubscriptionPricingTierIds.Families,
|
||||
details: {
|
||||
id: PersonalSubscriptionPricingTierIds.Families,
|
||||
name: "Families",
|
||||
description: "Families plan",
|
||||
availableCadences: ["annually"],
|
||||
passwordManager: {
|
||||
type: "packaged",
|
||||
annualPrice: 40,
|
||||
annualPricePerAdditionalStorageGB: 4,
|
||||
features: [
|
||||
{ key: "feature1", value: "Feature 1" },
|
||||
{ key: "feature2", value: "Feature 2" },
|
||||
],
|
||||
users: 6,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockReset(mockOrganizationBillingService);
|
||||
mockReset(mockSubscriberBillingClient);
|
||||
mockReset(mockTaxService);
|
||||
mockReset(mockLogService);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
UpgradePaymentService,
|
||||
|
||||
{
|
||||
provide: OrganizationBillingServiceAbstraction,
|
||||
useValue: mockOrganizationBillingService,
|
||||
},
|
||||
{ provide: SubscriberBillingClient, useValue: mockSubscriberBillingClient },
|
||||
{ provide: TaxServiceAbstraction, useValue: mockTaxService },
|
||||
{ provide: LogService, useValue: mockLogService },
|
||||
{ provide: ApiService, useValue: mockApiService },
|
||||
{ provide: SyncService, useValue: mockSyncService },
|
||||
],
|
||||
});
|
||||
|
||||
sut = TestBed.inject(UpgradePaymentService);
|
||||
});
|
||||
|
||||
describe("calculateEstimatedTax", () => {
|
||||
it("should calculate tax for premium plan", async () => {
|
||||
// Arrange
|
||||
const mockResponse = mock<PreviewInvoiceResponse>();
|
||||
mockResponse.taxAmount = 2.5;
|
||||
|
||||
mockTaxService.previewIndividualInvoice.mockResolvedValue(mockResponse);
|
||||
|
||||
// Act
|
||||
const result = await sut.calculateEstimatedTax(mockPremiumPlanDetails, mockBillingAddress);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(2.5);
|
||||
expect(mockTaxService.previewIndividualInvoice).toHaveBeenCalledWith({
|
||||
passwordManager: { additionalStorage: 0 },
|
||||
taxInformation: {
|
||||
postalCode: "12345",
|
||||
country: "US",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should calculate tax for families plan", async () => {
|
||||
// Arrange
|
||||
const mockResponse = mock<PreviewInvoiceResponse>();
|
||||
mockResponse.taxAmount = 5.0;
|
||||
|
||||
mockTaxService.previewOrganizationInvoice.mockResolvedValue(mockResponse);
|
||||
|
||||
// Act
|
||||
const result = await sut.calculateEstimatedTax(mockFamiliesPlanDetails, mockBillingAddress);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(5.0);
|
||||
expect(mockTaxService.previewOrganizationInvoice).toHaveBeenCalledWith({
|
||||
passwordManager: {
|
||||
additionalStorage: 0,
|
||||
plan: PlanType.FamiliesAnnually,
|
||||
seats: 6,
|
||||
},
|
||||
taxInformation: {
|
||||
postalCode: "12345",
|
||||
country: "US",
|
||||
taxId: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw and log error if personal tax calculation fails", async () => {
|
||||
// Arrange
|
||||
const error = new Error("Tax service error");
|
||||
mockTaxService.previewIndividualInvoice.mockRejectedValue(error);
|
||||
|
||||
// Act & Assert
|
||||
await expect(
|
||||
sut.calculateEstimatedTax(mockPremiumPlanDetails, mockBillingAddress),
|
||||
).rejects.toThrow();
|
||||
expect(mockLogService.error).toHaveBeenCalledWith("Tax calculation failed:", error);
|
||||
});
|
||||
|
||||
it("should throw and log error if organization tax calculation fails", async () => {
|
||||
// Arrange
|
||||
const error = new Error("Tax service error");
|
||||
mockTaxService.previewOrganizationInvoice.mockRejectedValue(error);
|
||||
// Act & Assert
|
||||
await expect(
|
||||
sut.calculateEstimatedTax(mockFamiliesPlanDetails, mockBillingAddress),
|
||||
).rejects.toThrow();
|
||||
expect(mockLogService.error).toHaveBeenCalledWith("Tax calculation failed:", error);
|
||||
});
|
||||
});
|
||||
|
||||
describe("upgradeToPremium", () => {
|
||||
it("should call subscriberBillingClient to purchase premium subscription and refresh data", async () => {
|
||||
// Arrange
|
||||
mockSubscriberBillingClient.purchasePremiumSubscription.mockResolvedValue();
|
||||
|
||||
// Act
|
||||
await sut.upgradeToPremium(mockSubscriber, mockTokenizedPaymentMethod, mockBillingAddress);
|
||||
|
||||
// Assert
|
||||
expect(mockSubscriberBillingClient.purchasePremiumSubscription).toHaveBeenCalledWith(
|
||||
mockSubscriber,
|
||||
mockTokenizedPaymentMethod,
|
||||
mockBillingAddress,
|
||||
);
|
||||
expect(mockApiService.refreshIdentityToken).toHaveBeenCalled();
|
||||
expect(mockSyncService.fullSync).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it("should throw error if payment method is incomplete", async () => {
|
||||
// Arrange
|
||||
const incompletePaymentMethod = { type: "card" } as TokenizedPaymentMethod;
|
||||
|
||||
// Act & Assert
|
||||
await expect(
|
||||
sut.upgradeToPremium(mockSubscriber, incompletePaymentMethod, mockBillingAddress),
|
||||
).rejects.toThrow("Payment method type or token is missing");
|
||||
});
|
||||
|
||||
it("should throw error if billing address is incomplete", async () => {
|
||||
// Arrange
|
||||
const incompleteBillingAddress = { country: "US", postalCode: null } as any;
|
||||
|
||||
// Act & Assert
|
||||
await expect(
|
||||
sut.upgradeToPremium(mockSubscriber, mockTokenizedPaymentMethod, incompleteBillingAddress),
|
||||
).rejects.toThrow("Billing address information is incomplete");
|
||||
});
|
||||
});
|
||||
|
||||
describe("upgradeToFamilies", () => {
|
||||
it("should call organizationBillingService to purchase subscription and refresh data", async () => {
|
||||
// Arrange
|
||||
mockOrganizationBillingService.purchaseSubscription.mockResolvedValue({
|
||||
id: "org-id",
|
||||
name: "Test Organization",
|
||||
billingEmail: "test@example.com",
|
||||
} as OrganizationResponse);
|
||||
|
||||
// Act
|
||||
await sut.upgradeToFamilies(
|
||||
mockSubscriber,
|
||||
mockFamiliesPlanDetails,
|
||||
mockTokenizedPaymentMethod,
|
||||
{
|
||||
organizationName: "Test Organization",
|
||||
billingAddress: mockBillingAddress,
|
||||
},
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(mockOrganizationBillingService.purchaseSubscription).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
organization: {
|
||||
name: "Test Organization",
|
||||
billingEmail: "test@example.com",
|
||||
},
|
||||
plan: {
|
||||
type: PlanType.FamiliesAnnually,
|
||||
passwordManagerSeats: 6,
|
||||
},
|
||||
payment: {
|
||||
paymentMethod: ["test-token", PaymentMethodType.Card],
|
||||
billing: {
|
||||
country: "US",
|
||||
postalCode: "12345",
|
||||
},
|
||||
},
|
||||
}),
|
||||
"user-id",
|
||||
);
|
||||
expect(mockApiService.refreshIdentityToken).toHaveBeenCalled();
|
||||
expect(mockSyncService.fullSync).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it("should throw error if password manager seats are 0", async () => {
|
||||
// Arrange
|
||||
const invalidPlanDetails: PlanDetails = {
|
||||
tier: PersonalSubscriptionPricingTierIds.Families,
|
||||
details: {
|
||||
passwordManager: {
|
||||
type: "packaged",
|
||||
users: 0,
|
||||
annualPrice: 0,
|
||||
features: [],
|
||||
annualPricePerAdditionalStorageGB: 0,
|
||||
},
|
||||
id: "families",
|
||||
name: "",
|
||||
description: "",
|
||||
availableCadences: ["annually"],
|
||||
},
|
||||
};
|
||||
|
||||
mockOrganizationBillingService.purchaseSubscription.mockRejectedValue(
|
||||
new Error("Seats must be greater than 0 for families plan"),
|
||||
);
|
||||
|
||||
// Act & Assert
|
||||
await expect(
|
||||
sut.upgradeToFamilies(mockSubscriber, invalidPlanDetails, mockTokenizedPaymentMethod, {
|
||||
organizationName: "Test Organization",
|
||||
billingAddress: mockBillingAddress,
|
||||
}),
|
||||
).rejects.toThrow("Seats must be greater than 0 for families plan");
|
||||
expect(mockOrganizationBillingService.purchaseSubscription).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should throw error if subscriber is not an account", async () => {
|
||||
const invalidSubscriber = { type: "organization" } as BitwardenSubscriber;
|
||||
|
||||
await expect(
|
||||
sut.upgradeToFamilies(
|
||||
invalidSubscriber,
|
||||
mockFamiliesPlanDetails,
|
||||
mockTokenizedPaymentMethod,
|
||||
{
|
||||
organizationName: "Test Organization",
|
||||
billingAddress: mockBillingAddress,
|
||||
},
|
||||
),
|
||||
).rejects.toThrow("Subscriber must be an account for families upgrade");
|
||||
});
|
||||
|
||||
it("should throw error if payment method is incomplete", async () => {
|
||||
const incompletePaymentMethod = { type: "card" } as TokenizedPaymentMethod;
|
||||
|
||||
await expect(
|
||||
sut.upgradeToFamilies(mockSubscriber, mockFamiliesPlanDetails, incompletePaymentMethod, {
|
||||
organizationName: "Test Organization",
|
||||
billingAddress: mockBillingAddress,
|
||||
}),
|
||||
).rejects.toThrow("Payment method type or token is missing");
|
||||
});
|
||||
|
||||
describe("tokenizablePaymentMethodToLegacyEnum", () => {
|
||||
it("should convert 'card' to PaymentMethodType.Card", () => {
|
||||
const result = sut.tokenizablePaymentMethodToLegacyEnum("card");
|
||||
expect(result).toBe(PaymentMethodType.Card);
|
||||
});
|
||||
|
||||
it("should convert 'bankAccount' to PaymentMethodType.BankAccount", () => {
|
||||
const result = sut.tokenizablePaymentMethodToLegacyEnum("bankAccount");
|
||||
expect(result).toBe(PaymentMethodType.BankAccount);
|
||||
});
|
||||
|
||||
it("should convert 'payPal' to PaymentMethodType.PayPal", () => {
|
||||
const result = sut.tokenizablePaymentMethodToLegacyEnum("payPal");
|
||||
expect(result).toBe(PaymentMethodType.PayPal);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,234 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationResponse } from "@bitwarden/common/admin-console/models/response/organization.response";
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import {
|
||||
OrganizationBillingServiceAbstraction,
|
||||
SubscriptionInformation,
|
||||
} from "@bitwarden/common/billing/abstractions";
|
||||
import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
|
||||
import { PaymentMethodType, PlanType } from "@bitwarden/common/billing/enums";
|
||||
import { PreviewIndividualInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-individual-invoice.request";
|
||||
import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
|
||||
import { SubscriberBillingClient } from "../../../../clients";
|
||||
import {
|
||||
BillingAddress,
|
||||
TokenizablePaymentMethod,
|
||||
TokenizedPaymentMethod,
|
||||
} from "../../../../payment/types";
|
||||
import { BitwardenSubscriber } from "../../../../types";
|
||||
import {
|
||||
PersonalSubscriptionPricingTier,
|
||||
PersonalSubscriptionPricingTierId,
|
||||
PersonalSubscriptionPricingTierIds,
|
||||
} from "../../../../types/subscription-pricing-tier";
|
||||
|
||||
type TaxInformation = {
|
||||
postalCode: string;
|
||||
country: string;
|
||||
taxId: string | null;
|
||||
};
|
||||
|
||||
export type PlanDetails = {
|
||||
tier: PersonalSubscriptionPricingTierId;
|
||||
details: PersonalSubscriptionPricingTier;
|
||||
};
|
||||
|
||||
export type PaymentFormValues = {
|
||||
organizationName?: string | null;
|
||||
billingAddress: {
|
||||
country: string;
|
||||
postalCode: string;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Service for handling payment submission and sales tax calculation for upgrade payment component
|
||||
*/
|
||||
@Injectable()
|
||||
export class UpgradePaymentService {
|
||||
constructor(
|
||||
private organizationBillingService: OrganizationBillingServiceAbstraction,
|
||||
private subscriberBillingClient: SubscriberBillingClient,
|
||||
private taxService: TaxServiceAbstraction,
|
||||
private logService: LogService,
|
||||
private apiService: ApiService,
|
||||
private syncService: SyncService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Calculate estimated tax for the selected plan
|
||||
*/
|
||||
async calculateEstimatedTax(
|
||||
planDetails: PlanDetails,
|
||||
billingAddress: Pick<BillingAddress, "country" | "postalCode">,
|
||||
): Promise<number> {
|
||||
try {
|
||||
const taxInformation: TaxInformation = {
|
||||
postalCode: billingAddress.postalCode,
|
||||
country: billingAddress.country,
|
||||
// This is null for now since we only process Families and Premium plans
|
||||
taxId: null,
|
||||
};
|
||||
|
||||
const isOrganizationPlan = planDetails.tier === PersonalSubscriptionPricingTierIds.Families;
|
||||
const isPremiumPlan = planDetails.tier === PersonalSubscriptionPricingTierIds.Premium;
|
||||
|
||||
let taxServiceCall: Promise<{ taxAmount: number }> | null = null;
|
||||
|
||||
if (isOrganizationPlan) {
|
||||
const seats = this.getPasswordManagerSeats(planDetails);
|
||||
if (seats === 0) {
|
||||
throw new Error("Seats must be greater than 0 for organization plan");
|
||||
}
|
||||
// Currently, only Families plan is supported for organization plans
|
||||
const request: PreviewOrganizationInvoiceRequest = {
|
||||
passwordManager: {
|
||||
additionalStorage: 0,
|
||||
plan: PlanType.FamiliesAnnually,
|
||||
seats: seats,
|
||||
},
|
||||
taxInformation,
|
||||
};
|
||||
|
||||
taxServiceCall = this.taxService.previewOrganizationInvoice(request);
|
||||
}
|
||||
|
||||
if (isPremiumPlan) {
|
||||
const request: PreviewIndividualInvoiceRequest = {
|
||||
passwordManager: { additionalStorage: 0 },
|
||||
taxInformation: {
|
||||
postalCode: billingAddress.postalCode,
|
||||
country: billingAddress.country,
|
||||
},
|
||||
};
|
||||
|
||||
taxServiceCall = this.taxService.previewIndividualInvoice(request);
|
||||
}
|
||||
|
||||
if (taxServiceCall === null) {
|
||||
throw new Error("Tax service call is not defined");
|
||||
}
|
||||
|
||||
const invoice = await taxServiceCall;
|
||||
return invoice.taxAmount;
|
||||
} catch (error: unknown) {
|
||||
this.logService.error("Tax calculation failed:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process premium upgrade
|
||||
*/
|
||||
async upgradeToPremium(
|
||||
subscriber: BitwardenSubscriber,
|
||||
paymentMethod: TokenizedPaymentMethod,
|
||||
billingAddress: Pick<BillingAddress, "country" | "postalCode">,
|
||||
): Promise<void> {
|
||||
this.validatePaymentAndBillingInfo(paymentMethod, billingAddress);
|
||||
|
||||
await this.subscriberBillingClient.purchasePremiumSubscription(
|
||||
subscriber,
|
||||
paymentMethod,
|
||||
billingAddress,
|
||||
);
|
||||
|
||||
await this.refreshAndSync();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process families upgrade
|
||||
*/
|
||||
async upgradeToFamilies(
|
||||
subscriber: BitwardenSubscriber,
|
||||
planDetails: PlanDetails,
|
||||
paymentMethod: TokenizedPaymentMethod,
|
||||
formValues: PaymentFormValues,
|
||||
): Promise<OrganizationResponse> {
|
||||
if (subscriber.type !== "account") {
|
||||
throw new Error("Subscriber must be an account for families upgrade");
|
||||
}
|
||||
const user = subscriber.data as Account;
|
||||
const billingAddress = formValues.billingAddress;
|
||||
|
||||
if (!formValues.organizationName) {
|
||||
throw new Error("Organization name is required for families upgrade");
|
||||
}
|
||||
|
||||
this.validatePaymentAndBillingInfo(paymentMethod, billingAddress);
|
||||
|
||||
const passwordManagerSeats = this.getPasswordManagerSeats(planDetails);
|
||||
|
||||
const subscriptionInformation: SubscriptionInformation = {
|
||||
organization: {
|
||||
name: formValues.organizationName,
|
||||
billingEmail: user.email, // Use account email as billing email
|
||||
},
|
||||
plan: {
|
||||
type: PlanType.FamiliesAnnually,
|
||||
passwordManagerSeats: passwordManagerSeats,
|
||||
},
|
||||
payment: {
|
||||
paymentMethod: [
|
||||
paymentMethod.token,
|
||||
this.tokenizablePaymentMethodToLegacyEnum(paymentMethod.type),
|
||||
],
|
||||
billing: {
|
||||
country: billingAddress.country,
|
||||
postalCode: billingAddress.postalCode,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await this.organizationBillingService.purchaseSubscription(
|
||||
subscriptionInformation,
|
||||
user.id,
|
||||
);
|
||||
await this.refreshAndSync();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert tokenizable payment method to legacy enum
|
||||
* note: this will be removed once another PR is merged
|
||||
*/
|
||||
tokenizablePaymentMethodToLegacyEnum(paymentMethod: TokenizablePaymentMethod): PaymentMethodType {
|
||||
switch (paymentMethod) {
|
||||
case "bankAccount":
|
||||
return PaymentMethodType.BankAccount;
|
||||
case "card":
|
||||
return PaymentMethodType.Card;
|
||||
case "payPal":
|
||||
return PaymentMethodType.PayPal;
|
||||
}
|
||||
}
|
||||
|
||||
private getPasswordManagerSeats(planDetails: PlanDetails): number {
|
||||
return "users" in planDetails.details.passwordManager
|
||||
? planDetails.details.passwordManager.users
|
||||
: 0;
|
||||
}
|
||||
|
||||
private validatePaymentAndBillingInfo(
|
||||
paymentMethod: TokenizedPaymentMethod,
|
||||
billingAddress: { country: string; postalCode: string },
|
||||
): void {
|
||||
if (!paymentMethod?.token || !paymentMethod?.type) {
|
||||
throw new Error("Payment method type or token is missing");
|
||||
}
|
||||
|
||||
if (!billingAddress?.country || !billingAddress?.postalCode) {
|
||||
throw new Error("Billing address information is incomplete");
|
||||
}
|
||||
}
|
||||
|
||||
private async refreshAndSync(): Promise<void> {
|
||||
await this.apiService.refreshIdentityToken();
|
||||
await this.syncService.fullSync(true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user