mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 06:13:38 +00:00
[PM-16937] Remove Billing Circular Dependency (#13085)
* Remove circular dependency between billing services and components * Removed `logService` from `billing-api.service.ts` * Resolved failed test * Removed @bitwarden/ui-common * Added optional `title` parameter to `BillingNotificationService` functions * Removed @bitwarden/platform from libs/common/tsconfig.json * Update apps/web/src/app/billing/services/billing-notification.service.spec.ts Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com> * Update apps/web/src/app/billing/services/billing-notification.service.spec.ts Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com> * Resolved build errors * Resolved issue where free trial banner wouldn't display if missing a payment method --------- Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
This commit is contained in:
@@ -24,6 +24,7 @@ import {
|
|||||||
switchMap,
|
switchMap,
|
||||||
takeUntil,
|
takeUntil,
|
||||||
tap,
|
tap,
|
||||||
|
catchError,
|
||||||
} from "rxjs/operators";
|
} from "rxjs/operators";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -76,6 +77,7 @@ import {
|
|||||||
PasswordRepromptService,
|
PasswordRepromptService,
|
||||||
} from "@bitwarden/vault";
|
} from "@bitwarden/vault";
|
||||||
|
|
||||||
|
import { BillingNotificationService } from "../../../billing/services/billing-notification.service";
|
||||||
import {
|
import {
|
||||||
ResellerWarning,
|
ResellerWarning,
|
||||||
ResellerWarningService,
|
ResellerWarningService,
|
||||||
@@ -256,6 +258,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
private organizationBillingService: OrganizationBillingServiceAbstraction,
|
private organizationBillingService: OrganizationBillingServiceAbstraction,
|
||||||
private resellerWarningService: ResellerWarningService,
|
private resellerWarningService: ResellerWarningService,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
|
private billingNotificationService: BillingNotificationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@@ -636,12 +639,18 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
combineLatest([
|
combineLatest([
|
||||||
of(org),
|
of(org),
|
||||||
this.organizationApiService.getSubscription(org.id),
|
this.organizationApiService.getSubscription(org.id),
|
||||||
this.organizationBillingService.getPaymentSource(org.id),
|
from(this.organizationBillingService.getPaymentSource(org.id)).pipe(
|
||||||
|
catchError((error: unknown) => {
|
||||||
|
this.billingNotificationService.handleError(error);
|
||||||
|
return of(null);
|
||||||
|
}),
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
map(([org, sub, paymentSource]) => {
|
map(([org, sub, paymentSource]) =>
|
||||||
return this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(org, sub, paymentSource);
|
this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(org, sub, paymentSource),
|
||||||
}),
|
),
|
||||||
|
filter((result) => result !== null),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.resellerWarning$ = organization$.pipe(
|
this.resellerWarning$ = organization$.pipe(
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv
|
|||||||
import { DialogService, ToastService } from "@bitwarden/components";
|
import { DialogService, ToastService } from "@bitwarden/components";
|
||||||
import { KeyService } from "@bitwarden/key-management";
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
|
import { BillingNotificationService } from "../services/billing-notification.service";
|
||||||
import { BillingSharedModule } from "../shared/billing-shared.module";
|
import { BillingSharedModule } from "../shared/billing-shared.module";
|
||||||
import { PaymentComponent } from "../shared/payment/payment.component";
|
import { PaymentComponent } from "../shared/payment/payment.component";
|
||||||
|
|
||||||
@@ -208,6 +209,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
|||||||
private taxService: TaxServiceAbstraction,
|
private taxService: TaxServiceAbstraction,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private organizationBillingService: OrganizationBillingService,
|
private organizationBillingService: OrganizationBillingService,
|
||||||
|
private billingNotificationService: BillingNotificationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
@@ -228,10 +230,14 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
|||||||
.organizations$(userId)
|
.organizations$(userId)
|
||||||
.pipe(getOrganizationById(this.organizationId)),
|
.pipe(getOrganizationById(this.organizationId)),
|
||||||
);
|
);
|
||||||
const { accountCredit, paymentSource } =
|
try {
|
||||||
await this.billingApiService.getOrganizationPaymentMethod(this.organizationId);
|
const { accountCredit, paymentSource } =
|
||||||
this.accountCredit = accountCredit;
|
await this.billingApiService.getOrganizationPaymentMethod(this.organizationId);
|
||||||
this.paymentSource = paymentSource;
|
this.accountCredit = accountCredit;
|
||||||
|
this.paymentSource = paymentSource;
|
||||||
|
} catch (error) {
|
||||||
|
this.billingNotificationService.handleError(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.selfHosted) {
|
if (!this.selfHosted) {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
|||||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||||
import { DialogService, ToastService } from "@bitwarden/components";
|
import { DialogService, ToastService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { BillingNotificationService } from "../../services/billing-notification.service";
|
||||||
import { TrialFlowService } from "../../services/trial-flow.service";
|
import { TrialFlowService } from "../../services/trial-flow.service";
|
||||||
import {
|
import {
|
||||||
AddCreditDialogResult,
|
AddCreditDialogResult,
|
||||||
@@ -66,6 +67,7 @@ export class OrganizationPaymentMethodComponent implements OnDestroy {
|
|||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
protected syncService: SyncService,
|
protected syncService: SyncService,
|
||||||
|
private billingNotificationService: BillingNotificationService,
|
||||||
) {
|
) {
|
||||||
this.activatedRoute.params
|
this.activatedRoute.params
|
||||||
.pipe(
|
.pipe(
|
||||||
@@ -115,47 +117,52 @@ export class OrganizationPaymentMethodComponent implements OnDestroy {
|
|||||||
|
|
||||||
protected load = async (): Promise<void> => {
|
protected load = async (): Promise<void> => {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
const { accountCredit, paymentSource, subscriptionStatus } =
|
try {
|
||||||
await this.billingApiService.getOrganizationPaymentMethod(this.organizationId);
|
const { accountCredit, paymentSource, subscriptionStatus } =
|
||||||
this.accountCredit = accountCredit;
|
await this.billingApiService.getOrganizationPaymentMethod(this.organizationId);
|
||||||
this.paymentSource = paymentSource;
|
this.accountCredit = accountCredit;
|
||||||
this.subscriptionStatus = subscriptionStatus;
|
this.paymentSource = paymentSource;
|
||||||
|
this.subscriptionStatus = subscriptionStatus;
|
||||||
|
|
||||||
if (this.organizationId) {
|
if (this.organizationId) {
|
||||||
const organizationSubscriptionPromise = this.organizationApiService.getSubscription(
|
const organizationSubscriptionPromise = this.organizationApiService.getSubscription(
|
||||||
this.organizationId,
|
this.organizationId,
|
||||||
);
|
);
|
||||||
const userId = await firstValueFrom(
|
const userId = await firstValueFrom(
|
||||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||||
);
|
);
|
||||||
const organizationPromise = await firstValueFrom(
|
const organizationPromise = await firstValueFrom(
|
||||||
this.organizationService
|
this.organizationService
|
||||||
.organizations$(userId)
|
.organizations$(userId)
|
||||||
.pipe(getOrganizationById(this.organizationId)),
|
.pipe(getOrganizationById(this.organizationId)),
|
||||||
);
|
);
|
||||||
|
|
||||||
[this.organizationSubscriptionResponse, this.organization] = await Promise.all([
|
[this.organizationSubscriptionResponse, this.organization] = await Promise.all([
|
||||||
organizationSubscriptionPromise,
|
organizationSubscriptionPromise,
|
||||||
organizationPromise,
|
organizationPromise,
|
||||||
]);
|
]);
|
||||||
this.freeTrialData = this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(
|
this.freeTrialData = this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(
|
||||||
this.organization,
|
this.organization,
|
||||||
this.organizationSubscriptionResponse,
|
this.organizationSubscriptionResponse,
|
||||||
paymentSource,
|
paymentSource,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
this.isUnpaid = this.subscriptionStatus === "unpaid" ?? false;
|
||||||
|
// 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;
|
||||||
}
|
}
|
||||||
this.isUnpaid = this.subscriptionStatus === "unpaid" ?? false;
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
this.loading = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
protected updatePaymentMethod = async (): Promise<void> => {
|
protected updatePaymentMethod = async (): Promise<void> => {
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { ToastService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { BillingNotificationService } from "./billing-notification.service";
|
||||||
|
|
||||||
|
describe("BillingNotificationService", () => {
|
||||||
|
let service: BillingNotificationService;
|
||||||
|
let logService: MockProxy<LogService>;
|
||||||
|
let toastService: MockProxy<ToastService>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
logService = mock<LogService>();
|
||||||
|
toastService = mock<ToastService>();
|
||||||
|
service = new BillingNotificationService(logService, toastService);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("handleError", () => {
|
||||||
|
it("should log error and show toast for ErrorResponse", () => {
|
||||||
|
const error = new ErrorResponse(["test error"], 400);
|
||||||
|
|
||||||
|
expect(() => service.handleError(error)).toThrow();
|
||||||
|
expect(logService.error).toHaveBeenCalledWith(error);
|
||||||
|
expect(toastService.showToast).toHaveBeenCalledWith({
|
||||||
|
variant: "error",
|
||||||
|
title: "",
|
||||||
|
message: error.getSingleMessage(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows error toast with the provided error", () => {
|
||||||
|
const error = new ErrorResponse(["test error"], 400);
|
||||||
|
|
||||||
|
expect(() => service.handleError(error, "Test Title")).toThrow();
|
||||||
|
expect(toastService.showToast).toHaveBeenCalledWith({
|
||||||
|
variant: "error",
|
||||||
|
title: "Test Title",
|
||||||
|
message: error.getSingleMessage(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should only log error for non-ErrorResponse", () => {
|
||||||
|
const error = new Error("test error");
|
||||||
|
|
||||||
|
expect(() => service.handleError(error)).toThrow();
|
||||||
|
expect(logService.error).toHaveBeenCalledWith(error);
|
||||||
|
expect(toastService.showToast).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("showSuccess", () => {
|
||||||
|
it("shows success toast with default title when provided title is empty", () => {
|
||||||
|
const message = "test message";
|
||||||
|
service.showSuccess(message);
|
||||||
|
|
||||||
|
expect(toastService.showToast).toHaveBeenCalledWith({
|
||||||
|
variant: "success",
|
||||||
|
title: "",
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show success toast with custom title", () => {
|
||||||
|
const message = "test message";
|
||||||
|
service.showSuccess(message, "Success Title");
|
||||||
|
|
||||||
|
expect(toastService.showToast).toHaveBeenCalledWith({
|
||||||
|
variant: "success",
|
||||||
|
title: "Success Title",
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
|
||||||
|
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { ToastService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: "root",
|
||||||
|
})
|
||||||
|
export class BillingNotificationService {
|
||||||
|
constructor(
|
||||||
|
private logService: LogService,
|
||||||
|
private toastService: ToastService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
handleError(error: unknown, title: string = "") {
|
||||||
|
this.logService.error(error);
|
||||||
|
if (error instanceof ErrorResponse) {
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: title,
|
||||||
|
message: error.getSingleMessage(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccess(message: string, title: string = "") {
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "success",
|
||||||
|
title: title,
|
||||||
|
message: message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
@NgModule({})
|
import { BillingNotificationService } from "./billing-notification.service";
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
providers: [BillingNotificationService],
|
||||||
|
})
|
||||||
export class BillingServicesModule {}
|
export class BillingServicesModule {}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
take,
|
take,
|
||||||
takeUntil,
|
takeUntil,
|
||||||
tap,
|
tap,
|
||||||
|
catchError,
|
||||||
} from "rxjs/operators";
|
} from "rxjs/operators";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -80,6 +81,7 @@ import {
|
|||||||
CollectionDialogTabType,
|
CollectionDialogTabType,
|
||||||
openCollectionDialog,
|
openCollectionDialog,
|
||||||
} from "../../admin-console/organizations/shared/components/collection-dialog";
|
} from "../../admin-console/organizations/shared/components/collection-dialog";
|
||||||
|
import { BillingNotificationService } from "../../billing/services/billing-notification.service";
|
||||||
import { TrialFlowService } from "../../billing/services/trial-flow.service";
|
import { TrialFlowService } from "../../billing/services/trial-flow.service";
|
||||||
import { FreeTrial } from "../../billing/types/free-trial";
|
import { FreeTrial } from "../../billing/types/free-trial";
|
||||||
import { SharedModule } from "../../shared/shared.module";
|
import { SharedModule } from "../../shared/shared.module";
|
||||||
@@ -213,20 +215,25 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
ownerOrgs.map((org) =>
|
ownerOrgs.map((org) =>
|
||||||
combineLatest([
|
combineLatest([
|
||||||
this.organizationApiService.getSubscription(org.id),
|
this.organizationApiService.getSubscription(org.id),
|
||||||
this.organizationBillingService.getPaymentSource(org.id),
|
from(this.organizationBillingService.getPaymentSource(org.id)).pipe(
|
||||||
|
catchError((error: unknown) => {
|
||||||
|
this.billingNotificationService.handleError(error);
|
||||||
|
return of(null);
|
||||||
|
}),
|
||||||
|
),
|
||||||
]).pipe(
|
]).pipe(
|
||||||
map(([subscription, paymentSource]) => {
|
map(([subscription, paymentSource]) =>
|
||||||
return this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(
|
this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(
|
||||||
org,
|
org,
|
||||||
subscription,
|
subscription,
|
||||||
paymentSource,
|
paymentSource,
|
||||||
);
|
),
|
||||||
}),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
map((results) => results.filter((result) => result.shownBanner)),
|
map((results) => results.filter((result) => result !== null && result.shownBanner)),
|
||||||
shareReplay({ refCount: false, bufferSize: 1 }),
|
shareReplay({ refCount: false, bufferSize: 1 }),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -262,6 +269,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
protected billingApiService: BillingApiServiceAbstraction,
|
protected billingApiService: BillingApiServiceAbstraction,
|
||||||
private trialFlowService: TrialFlowService,
|
private trialFlowService: TrialFlowService,
|
||||||
private organizationBillingService: OrganizationBillingServiceAbstraction,
|
private organizationBillingService: OrganizationBillingServiceAbstraction,
|
||||||
|
private billingNotificationService: BillingNotificationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { map } from "rxjs";
|
|||||||
|
|
||||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||||
import { InvoiceResponse } from "@bitwarden/common/billing/models/response/invoices.response";
|
import { InvoiceResponse } from "@bitwarden/common/billing/models/response/invoices.response";
|
||||||
|
import { BillingNotificationService } from "@bitwarden/web-vault/app/billing/services/billing-notification.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
templateUrl: "./provider-billing-history.component.html",
|
templateUrl: "./provider-billing-history.component.html",
|
||||||
@@ -19,6 +20,7 @@ export class ProviderBillingHistoryComponent {
|
|||||||
private activatedRoute: ActivatedRoute,
|
private activatedRoute: ActivatedRoute,
|
||||||
private billingApiService: BillingApiServiceAbstraction,
|
private billingApiService: BillingApiServiceAbstraction,
|
||||||
private datePipe: DatePipe,
|
private datePipe: DatePipe,
|
||||||
|
private billingNotificationService: BillingNotificationService,
|
||||||
) {
|
) {
|
||||||
this.activatedRoute.params
|
this.activatedRoute.params
|
||||||
.pipe(
|
.pipe(
|
||||||
@@ -30,13 +32,27 @@ export class ProviderBillingHistoryComponent {
|
|||||||
.subscribe();
|
.subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
getClientInvoiceReport = (invoiceId: string) =>
|
getClientInvoiceReport = async (invoiceId: string) => {
|
||||||
this.billingApiService.getProviderClientInvoiceReport(this.providerId, invoiceId);
|
try {
|
||||||
|
return await this.billingApiService.getProviderClientInvoiceReport(
|
||||||
|
this.providerId,
|
||||||
|
invoiceId,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.billingNotificationService.handleError(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
getClientInvoiceReportName = (invoice: InvoiceResponse) => {
|
getClientInvoiceReportName = (invoice: InvoiceResponse) => {
|
||||||
const date = this.datePipe.transform(invoice.date, "yyyyMMdd");
|
const date = this.datePipe.transform(invoice.date, "yyyyMMdd");
|
||||||
return `bitwarden_provider-billing-history_${date}_${invoice.number}`;
|
return `bitwarden_provider-billing-history_${date}_${invoice.number}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
getInvoices = async () => await this.billingApiService.getProviderInvoices(this.providerId);
|
getInvoices = async () => {
|
||||||
|
try {
|
||||||
|
return await this.billingApiService.getProviderInvoices(this.providerId);
|
||||||
|
} catch (error) {
|
||||||
|
this.billingNotificationService.handleError(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstract
|
|||||||
import { UpdateClientOrganizationRequest } from "@bitwarden/common/billing/models/request/update-client-organization.request";
|
import { UpdateClientOrganizationRequest } from "@bitwarden/common/billing/models/request/update-client-organization.request";
|
||||||
import { ProviderPlanResponse } from "@bitwarden/common/billing/models/response/provider-subscription-response";
|
import { ProviderPlanResponse } from "@bitwarden/common/billing/models/response/provider-subscription-response";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { DialogService, ToastService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
import { BillingNotificationService } from "@bitwarden/web-vault/app/billing/services/billing-notification.service";
|
||||||
|
|
||||||
type ManageClientSubscriptionDialogParams = {
|
type ManageClientSubscriptionDialogParams = {
|
||||||
organization: ProviderOrganizationOrganizationDetailsResponse;
|
organization: ProviderOrganizationOrganizationDetailsResponse;
|
||||||
@@ -56,30 +57,34 @@ export class ManageClientSubscriptionDialogComponent implements OnInit {
|
|||||||
@Inject(DIALOG_DATA) protected dialogParams: ManageClientSubscriptionDialogParams,
|
@Inject(DIALOG_DATA) protected dialogParams: ManageClientSubscriptionDialogParams,
|
||||||
private dialogRef: DialogRef<ManageClientSubscriptionDialogResultType>,
|
private dialogRef: DialogRef<ManageClientSubscriptionDialogResultType>,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private toastService: ToastService,
|
private billingNotificationService: BillingNotificationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
const response = await this.billingApiService.getProviderSubscription(
|
try {
|
||||||
this.dialogParams.provider.id,
|
const response = await this.billingApiService.getProviderSubscription(
|
||||||
);
|
this.dialogParams.provider.id,
|
||||||
|
);
|
||||||
|
|
||||||
this.providerPlan = response.plans.find(
|
this.providerPlan = response.plans.find(
|
||||||
(plan) => plan.planName === this.dialogParams.organization.plan,
|
(plan) => plan.planName === this.dialogParams.organization.plan,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.assignedSeats = this.providerPlan.assignedSeats;
|
this.assignedSeats = this.providerPlan.assignedSeats;
|
||||||
this.openSeats = this.providerPlan.seatMinimum - this.providerPlan.assignedSeats;
|
this.openSeats = this.providerPlan.seatMinimum - this.providerPlan.assignedSeats;
|
||||||
this.purchasedSeats = this.providerPlan.purchasedSeats;
|
this.purchasedSeats = this.providerPlan.purchasedSeats;
|
||||||
this.seatMinimum = this.providerPlan.seatMinimum;
|
this.seatMinimum = this.providerPlan.seatMinimum;
|
||||||
|
|
||||||
this.formGroup.controls.assignedSeats.addValidators(
|
this.formGroup.controls.assignedSeats.addValidators(
|
||||||
this.isServiceUserWithPurchasedSeats
|
this.isServiceUserWithPurchasedSeats
|
||||||
? this.createPurchasedSeatsValidator()
|
? this.createPurchasedSeatsValidator()
|
||||||
: this.createUnassignedSeatsValidator(),
|
: this.createUnassignedSeatsValidator(),
|
||||||
);
|
);
|
||||||
|
} catch (error) {
|
||||||
this.loading = false;
|
this.billingNotificationService.handleError(error);
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
submit = async () => {
|
submit = async () => {
|
||||||
@@ -91,24 +96,25 @@ export class ManageClientSubscriptionDialogComponent implements OnInit {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const request = new UpdateClientOrganizationRequest();
|
try {
|
||||||
request.assignedSeats = this.formGroup.value.assignedSeats;
|
const request = new UpdateClientOrganizationRequest();
|
||||||
request.name = this.dialogParams.organization.organizationName;
|
request.assignedSeats = this.formGroup.value.assignedSeats;
|
||||||
|
request.name = this.dialogParams.organization.organizationName;
|
||||||
|
|
||||||
await this.billingApiService.updateProviderClientOrganization(
|
await this.billingApiService.updateProviderClientOrganization(
|
||||||
this.dialogParams.provider.id,
|
this.dialogParams.provider.id,
|
||||||
this.dialogParams.organization.id,
|
this.dialogParams.organization.id,
|
||||||
request,
|
request,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.toastService.showToast({
|
this.billingNotificationService.showSuccess(this.i18nService.t("subscriptionUpdated"));
|
||||||
variant: "success",
|
|
||||||
title: null,
|
|
||||||
message: this.i18nService.t("subscriptionUpdated"),
|
|
||||||
});
|
|
||||||
|
|
||||||
this.loading = false;
|
this.dialogRef.close(this.ResultType.Submitted);
|
||||||
this.dialogRef.close(this.ResultType.Submitted);
|
} catch (error) {
|
||||||
|
this.billingNotificationService.handleError(error);
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
createPurchasedSeatsValidator =
|
createPurchasedSeatsValidator =
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
ToastService,
|
ToastService,
|
||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/shared";
|
import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/shared";
|
||||||
|
import { BillingNotificationService } from "@bitwarden/web-vault/app/billing/services/billing-notification.service";
|
||||||
import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module";
|
import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module";
|
||||||
|
|
||||||
import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service";
|
import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service";
|
||||||
@@ -83,6 +84,7 @@ export class ManageClientsComponent {
|
|||||||
private validationService: ValidationService,
|
private validationService: ValidationService,
|
||||||
private webProviderService: WebProviderService,
|
private webProviderService: WebProviderService,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
|
private billingNotificationService: BillingNotificationService,
|
||||||
) {
|
) {
|
||||||
this.activatedRoute.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((queryParams) => {
|
this.activatedRoute.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((queryParams) => {
|
||||||
this.searchControl.setValue(queryParams.search);
|
this.searchControl.setValue(queryParams.search);
|
||||||
@@ -120,13 +122,17 @@ export class ManageClientsComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
this.provider = await firstValueFrom(this.providerService.get$(this.providerId));
|
try {
|
||||||
this.isProviderAdmin = this.provider?.type === ProviderUserType.ProviderAdmin;
|
this.provider = await firstValueFrom(this.providerService.get$(this.providerId));
|
||||||
this.dataSource.data = (
|
this.isProviderAdmin = this.provider?.type === ProviderUserType.ProviderAdmin;
|
||||||
await this.billingApiService.getProviderClientOrganizations(this.providerId)
|
this.dataSource.data = (
|
||||||
).data;
|
await this.billingApiService.getProviderClientOrganizations(this.providerId)
|
||||||
this.plans = (await this.billingApiService.getPlans()).data;
|
).data;
|
||||||
this.loading = false;
|
this.plans = (await this.billingApiService.getPlans()).data;
|
||||||
|
this.loading = false;
|
||||||
|
} catch (error) {
|
||||||
|
this.billingNotificationService.handleError(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addExistingOrganization = async () => {
|
addExistingOrganization = async () => {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
ProviderSubscriptionResponse,
|
ProviderSubscriptionResponse,
|
||||||
} from "@bitwarden/common/billing/models/response/provider-subscription-response";
|
} from "@bitwarden/common/billing/models/response/provider-subscription-response";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { ToastService } from "@bitwarden/components";
|
import { BillingNotificationService } from "@bitwarden/web-vault/app/billing/services/billing-notification.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-provider-subscription",
|
selector: "app-provider-subscription",
|
||||||
@@ -33,7 +33,7 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy {
|
|||||||
private billingApiService: BillingApiServiceAbstraction,
|
private billingApiService: BillingApiServiceAbstraction,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private toastService: ToastService,
|
private billingNotificationService: BillingNotificationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@@ -54,20 +54,26 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.subscription = await this.billingApiService.getProviderSubscription(this.providerId);
|
try {
|
||||||
this.totalCost =
|
this.subscription = await this.billingApiService.getProviderSubscription(this.providerId);
|
||||||
((100 - this.subscription.discountPercentage) / 100) * this.sumCost(this.subscription.plans);
|
this.totalCost =
|
||||||
this.loading = false;
|
((100 - this.subscription.discountPercentage) / 100) *
|
||||||
|
this.sumCost(this.subscription.plans);
|
||||||
|
} catch (error) {
|
||||||
|
this.billingNotificationService.handleError(error);
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updateTaxInformation = async (taxInformation: TaxInformation) => {
|
protected updateTaxInformation = async (taxInformation: TaxInformation) => {
|
||||||
const request = ExpandedTaxInfoUpdateRequest.From(taxInformation);
|
try {
|
||||||
await this.billingApiService.updateProviderTaxInformation(this.providerId, request);
|
const request = ExpandedTaxInfoUpdateRequest.From(taxInformation);
|
||||||
this.toastService.showToast({
|
await this.billingApiService.updateProviderTaxInformation(this.providerId, request);
|
||||||
variant: "success",
|
this.billingNotificationService.showSuccess(this.i18nService.t("updatedTaxInformation"));
|
||||||
title: null,
|
} catch (error) {
|
||||||
message: this.i18nService.t("updatedTaxInformation"),
|
this.billingNotificationService.handleError(error);
|
||||||
});
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
protected getFormattedCost(
|
protected getFormattedCost(
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import {
|
|||||||
firstValueFrom,
|
firstValueFrom,
|
||||||
of,
|
of,
|
||||||
filter,
|
filter,
|
||||||
|
catchError,
|
||||||
|
from,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||||
@@ -32,6 +34,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
|||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
import { BillingNotificationService } from "@bitwarden/web-vault/app/billing/services/billing-notification.service";
|
||||||
import { TrialFlowService } from "@bitwarden/web-vault/app/billing/services/trial-flow.service";
|
import { TrialFlowService } from "@bitwarden/web-vault/app/billing/services/trial-flow.service";
|
||||||
import { FreeTrial } from "@bitwarden/web-vault/app/billing/types/free-trial";
|
import { FreeTrial } from "@bitwarden/web-vault/app/billing/types/free-trial";
|
||||||
|
|
||||||
@@ -126,6 +129,7 @@ export class OverviewComponent implements OnInit, OnDestroy {
|
|||||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||||
private trialFlowService: TrialFlowService,
|
private trialFlowService: TrialFlowService,
|
||||||
private organizationBillingService: OrganizationBillingServiceAbstraction,
|
private organizationBillingService: OrganizationBillingServiceAbstraction,
|
||||||
|
private billingNotificationService: BillingNotificationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@@ -161,12 +165,18 @@ export class OverviewComponent implements OnInit, OnDestroy {
|
|||||||
combineLatest([
|
combineLatest([
|
||||||
of(org),
|
of(org),
|
||||||
this.organizationApiService.getSubscription(org.id),
|
this.organizationApiService.getSubscription(org.id),
|
||||||
this.organizationBillingService.getPaymentSource(org.id),
|
from(this.organizationBillingService.getPaymentSource(org.id)).pipe(
|
||||||
|
catchError((error: unknown) => {
|
||||||
|
this.billingNotificationService.handleError(error);
|
||||||
|
return of(null);
|
||||||
|
}),
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
map(([org, sub, paymentSource]) => {
|
map(([org, sub, paymentSource]) => {
|
||||||
return this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(org, sub, paymentSource);
|
return this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(org, sub, paymentSource);
|
||||||
}),
|
}),
|
||||||
|
filter((result) => result !== null),
|
||||||
takeUntil(this.destroy$),
|
takeUntil(this.destroy$),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1284,7 +1284,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
safeProvider({
|
safeProvider({
|
||||||
provide: BillingApiServiceAbstraction,
|
provide: BillingApiServiceAbstraction,
|
||||||
useClass: BillingApiService,
|
useClass: BillingApiService,
|
||||||
deps: [ApiServiceAbstraction, LogService, ToastService],
|
deps: [ApiServiceAbstraction],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: TaxServiceAbstraction,
|
provide: TaxServiceAbstraction,
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { ToastService } from "@bitwarden/components";
|
|
||||||
|
|
||||||
import { ApiService } from "../../abstractions/api.service";
|
import { ApiService } from "../../abstractions/api.service";
|
||||||
import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request";
|
import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request";
|
||||||
import { ProviderOrganizationOrganizationDetailsResponse } from "../../admin-console/models/response/provider/provider-organization.response";
|
import { ProviderOrganizationOrganizationDetailsResponse } from "../../admin-console/models/response/provider/provider-organization.response";
|
||||||
import { ErrorResponse } from "../../models/response/error.response";
|
|
||||||
import { ListResponse } from "../../models/response/list.response";
|
import { ListResponse } from "../../models/response/list.response";
|
||||||
import { LogService } from "../../platform/abstractions/log.service";
|
|
||||||
import { BillingApiServiceAbstraction } from "../abstractions";
|
import { BillingApiServiceAbstraction } from "../abstractions";
|
||||||
import { PaymentMethodType } from "../enums";
|
import { PaymentMethodType } from "../enums";
|
||||||
import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request";
|
import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request";
|
||||||
@@ -23,11 +20,7 @@ import { PlanResponse } from "../models/response/plan.response";
|
|||||||
import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response";
|
import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response";
|
||||||
|
|
||||||
export class BillingApiService implements BillingApiServiceAbstraction {
|
export class BillingApiService implements BillingApiServiceAbstraction {
|
||||||
constructor(
|
constructor(private apiService: ApiService) {}
|
||||||
private apiService: ApiService,
|
|
||||||
private logService: LogService,
|
|
||||||
private toastService: ToastService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
cancelOrganizationSubscription(
|
cancelOrganizationSubscription(
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
@@ -89,14 +82,12 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getOrganizationPaymentMethod(organizationId: string): Promise<PaymentMethodResponse> {
|
async getOrganizationPaymentMethod(organizationId: string): Promise<PaymentMethodResponse> {
|
||||||
const response = await this.execute(() =>
|
const response = await this.apiService.send(
|
||||||
this.apiService.send(
|
"GET",
|
||||||
"GET",
|
"/organizations/" + organizationId + "/billing/payment-method",
|
||||||
"/organizations/" + organizationId + "/billing/payment-method",
|
null,
|
||||||
null,
|
true,
|
||||||
true,
|
true,
|
||||||
true,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return new PaymentMethodResponse(response);
|
return new PaymentMethodResponse(response);
|
||||||
}
|
}
|
||||||
@@ -120,34 +111,34 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
|||||||
async getProviderClientOrganizations(
|
async getProviderClientOrganizations(
|
||||||
providerId: string,
|
providerId: string,
|
||||||
): Promise<ListResponse<ProviderOrganizationOrganizationDetailsResponse>> {
|
): Promise<ListResponse<ProviderOrganizationOrganizationDetailsResponse>> {
|
||||||
const response = await this.execute(() =>
|
const response = await this.apiService.send(
|
||||||
this.apiService.send("GET", "/providers/" + providerId + "/organizations", null, true, true),
|
"GET",
|
||||||
|
"/providers/" + providerId + "/organizations",
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
return new ListResponse(response, ProviderOrganizationOrganizationDetailsResponse);
|
return new ListResponse(response, ProviderOrganizationOrganizationDetailsResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProviderInvoices(providerId: string): Promise<InvoicesResponse> {
|
async getProviderInvoices(providerId: string): Promise<InvoicesResponse> {
|
||||||
const response = await this.execute(() =>
|
const response = await this.apiService.send(
|
||||||
this.apiService.send(
|
"GET",
|
||||||
"GET",
|
"/providers/" + providerId + "/billing/invoices",
|
||||||
"/providers/" + providerId + "/billing/invoices",
|
null,
|
||||||
null,
|
true,
|
||||||
true,
|
true,
|
||||||
true,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return new InvoicesResponse(response);
|
return new InvoicesResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProviderSubscription(providerId: string): Promise<ProviderSubscriptionResponse> {
|
async getProviderSubscription(providerId: string): Promise<ProviderSubscriptionResponse> {
|
||||||
const response = await this.execute(() =>
|
const response = await this.apiService.send(
|
||||||
this.apiService.send(
|
"GET",
|
||||||
"GET",
|
"/providers/" + providerId + "/billing/subscription",
|
||||||
"/providers/" + providerId + "/billing/subscription",
|
null,
|
||||||
null,
|
true,
|
||||||
true,
|
true,
|
||||||
true,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return new ProviderSubscriptionResponse(response);
|
return new ProviderSubscriptionResponse(response);
|
||||||
}
|
}
|
||||||
@@ -227,20 +218,4 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async execute(request: () => Promise<any>): Promise<any> {
|
|
||||||
try {
|
|
||||||
return await request();
|
|
||||||
} catch (error) {
|
|
||||||
this.logService.error(error);
|
|
||||||
if (error instanceof ErrorResponse) {
|
|
||||||
this.toastService.showToast({
|
|
||||||
variant: "error",
|
|
||||||
title: null,
|
|
||||||
message: error.getSingleMessage(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,8 @@
|
|||||||
"@bitwarden/auth/common": ["../auth/src/common"],
|
"@bitwarden/auth/common": ["../auth/src/common"],
|
||||||
// TODO: Remove once circular dependencies in admin-console, auth and key-management are resolved
|
// TODO: Remove once circular dependencies in admin-console, auth and key-management are resolved
|
||||||
"@bitwarden/common/*": ["../common/src/*"],
|
"@bitwarden/common/*": ["../common/src/*"],
|
||||||
// TODO: Remove once billing stops depending on components
|
|
||||||
"@bitwarden/components": ["../components/src"],
|
|
||||||
"@bitwarden/key-management": ["../key-management/src"],
|
"@bitwarden/key-management": ["../key-management/src"],
|
||||||
"@bitwarden/vault-export-core": ["../tools/export/vault-export/vault-export-core/src"],
|
"@bitwarden/vault-export-core": ["../tools/export/vault-export/vault-export-core/src"]
|
||||||
"@bitwarden/platform": ["../platform/src"],
|
|
||||||
// TODO: Remove once billing stops depending on components
|
|
||||||
"@bitwarden/ui-common": ["../ui/common/src"]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src", "spec", "./custom-matchers.d.ts", "../key-management/src/index.ts"],
|
"include": ["src", "spec", "./custom-matchers.d.ts", "../key-management/src/index.ts"],
|
||||||
|
|||||||
Reference in New Issue
Block a user