mirror of
https://github.com/bitwarden/browser
synced 2026-02-10 05:30:01 +00:00
Merge branch 'main' into auth/pm-9115/implement-view-data-persistence-in-2FA-flows
This commit is contained in:
@@ -21,8 +21,7 @@ describe("InlineMenuFieldQualificationService", () => {
|
||||
});
|
||||
|
||||
describe("isFieldForLoginForm", () => {
|
||||
it("does not disqualify totp fields for premium users with flag set to true", () => {
|
||||
inlineMenuFieldQualificationService["inlineMenuTotpFeatureFlag"] = true;
|
||||
it("does not disqualify totp fields for premium users", () => {
|
||||
inlineMenuFieldQualificationService["premiumEnabled"] = true;
|
||||
const field = mock<AutofillField>({
|
||||
type: "text",
|
||||
@@ -37,24 +36,7 @@ describe("InlineMenuFieldQualificationService", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("disqualifies totp fields for premium users with flag set to false", () => {
|
||||
inlineMenuFieldQualificationService["inlineMenuTotpFeatureFlag"] = false;
|
||||
inlineMenuFieldQualificationService["inlineMenuTotpFeatureFlag"] = true;
|
||||
const field = mock<AutofillField>({
|
||||
type: "text",
|
||||
autoCompleteType: "one-time-code",
|
||||
htmlName: "totp",
|
||||
htmlID: "totp",
|
||||
placeholder: "totp",
|
||||
});
|
||||
|
||||
expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("disqualifies totp fields for non-premium users with flag set to true", () => {
|
||||
inlineMenuFieldQualificationService["inlineMenuTotpFeatureFlag"] = true;
|
||||
it("disqualifies totp fields for non-premium users", () => {
|
||||
inlineMenuFieldQualificationService["premiumEnabled"] = false;
|
||||
const field = mock<AutofillField>({
|
||||
type: "text",
|
||||
|
||||
@@ -151,17 +151,14 @@ export class InlineMenuFieldQualificationService
|
||||
]);
|
||||
private totpFieldAutocompleteValue = "one-time-code";
|
||||
private inlineMenuFieldQualificationFlagSet = false;
|
||||
private inlineMenuTotpFeatureFlag = false;
|
||||
private premiumEnabled = false;
|
||||
|
||||
constructor() {
|
||||
void Promise.all([
|
||||
sendExtensionMessage("getInlineMenuFieldQualificationFeatureFlag"),
|
||||
sendExtensionMessage("getInlineMenuTotpFeatureFlag"),
|
||||
sendExtensionMessage("getUserPremiumStatus"),
|
||||
]).then(([fieldQualificationFlag, totpFeatureFlag, premiumStatus]) => {
|
||||
]).then(([fieldQualificationFlag, premiumStatus]) => {
|
||||
this.inlineMenuFieldQualificationFlagSet = !!fieldQualificationFlag?.result;
|
||||
this.inlineMenuTotpFeatureFlag = !!totpFeatureFlag?.result;
|
||||
this.premiumEnabled = !!premiumStatus?.result;
|
||||
});
|
||||
}
|
||||
@@ -180,7 +177,7 @@ export class InlineMenuFieldQualificationService
|
||||
/**
|
||||
* Totp inline menu is available only for premium users.
|
||||
*/
|
||||
if (this.inlineMenuTotpFeatureFlag && this.premiumEnabled) {
|
||||
if (this.premiumEnabled) {
|
||||
const isTotpField = this.isTotpField(field);
|
||||
// Autofill does not fill totp inputs with a "password" `type` attribute value
|
||||
const passwordType = field.type === "password";
|
||||
|
||||
@@ -78,7 +78,6 @@ export default class RuntimeBackground {
|
||||
BiometricsCommands.GetBiometricsStatusForUser,
|
||||
"getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag",
|
||||
"getInlineMenuFieldQualificationFeatureFlag",
|
||||
"getInlineMenuTotpFeatureFlag",
|
||||
"getUserPremiumStatus",
|
||||
];
|
||||
|
||||
@@ -217,9 +216,6 @@ export default class RuntimeBackground {
|
||||
);
|
||||
return result;
|
||||
}
|
||||
case "getInlineMenuTotpFeatureFlag": {
|
||||
return await this.configService.getFeatureFlag(FeatureFlag.InlineMenuTotp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
switchMap,
|
||||
takeUntil,
|
||||
tap,
|
||||
catchError,
|
||||
} from "rxjs/operators";
|
||||
|
||||
import {
|
||||
@@ -76,6 +77,7 @@ import {
|
||||
PasswordRepromptService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { BillingNotificationService } from "../../../billing/services/billing-notification.service";
|
||||
import {
|
||||
ResellerWarning,
|
||||
ResellerWarningService,
|
||||
@@ -256,6 +258,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
private organizationBillingService: OrganizationBillingServiceAbstraction,
|
||||
private resellerWarningService: ResellerWarningService,
|
||||
private accountService: AccountService,
|
||||
private billingNotificationService: BillingNotificationService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -636,12 +639,18 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
combineLatest([
|
||||
of(org),
|
||||
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]) => {
|
||||
return this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(org, sub, paymentSource);
|
||||
}),
|
||||
map(([org, sub, paymentSource]) =>
|
||||
this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(org, sub, paymentSource),
|
||||
),
|
||||
filter((result) => result !== null),
|
||||
);
|
||||
|
||||
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 { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { BillingNotificationService } from "../services/billing-notification.service";
|
||||
import { BillingSharedModule } from "../shared/billing-shared.module";
|
||||
import { PaymentComponent } from "../shared/payment/payment.component";
|
||||
|
||||
@@ -208,6 +209,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
private taxService: TaxServiceAbstraction,
|
||||
private accountService: AccountService,
|
||||
private organizationBillingService: OrganizationBillingService,
|
||||
private billingNotificationService: BillingNotificationService,
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
@@ -228,10 +230,14 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
.organizations$(userId)
|
||||
.pipe(getOrganizationById(this.organizationId)),
|
||||
);
|
||||
const { accountCredit, paymentSource } =
|
||||
await this.billingApiService.getOrganizationPaymentMethod(this.organizationId);
|
||||
this.accountCredit = accountCredit;
|
||||
this.paymentSource = paymentSource;
|
||||
try {
|
||||
const { accountCredit, paymentSource } =
|
||||
await this.billingApiService.getOrganizationPaymentMethod(this.organizationId);
|
||||
this.accountCredit = accountCredit;
|
||||
this.paymentSource = paymentSource;
|
||||
} catch (error) {
|
||||
this.billingNotificationService.handleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.selfHosted) {
|
||||
|
||||
@@ -23,6 +23,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { BillingNotificationService } from "../../services/billing-notification.service";
|
||||
import { TrialFlowService } from "../../services/trial-flow.service";
|
||||
import {
|
||||
AddCreditDialogResult,
|
||||
@@ -66,6 +67,7 @@ export class OrganizationPaymentMethodComponent implements OnDestroy {
|
||||
private organizationService: OrganizationService,
|
||||
private accountService: AccountService,
|
||||
protected syncService: SyncService,
|
||||
private billingNotificationService: BillingNotificationService,
|
||||
) {
|
||||
this.activatedRoute.params
|
||||
.pipe(
|
||||
@@ -115,47 +117,52 @@ export class OrganizationPaymentMethodComponent implements OnDestroy {
|
||||
|
||||
protected load = async (): Promise<void> => {
|
||||
this.loading = true;
|
||||
const { accountCredit, paymentSource, subscriptionStatus } =
|
||||
await this.billingApiService.getOrganizationPaymentMethod(this.organizationId);
|
||||
this.accountCredit = accountCredit;
|
||||
this.paymentSource = paymentSource;
|
||||
this.subscriptionStatus = subscriptionStatus;
|
||||
try {
|
||||
const { accountCredit, paymentSource, subscriptionStatus } =
|
||||
await this.billingApiService.getOrganizationPaymentMethod(this.organizationId);
|
||||
this.accountCredit = accountCredit;
|
||||
this.paymentSource = paymentSource;
|
||||
this.subscriptionStatus = subscriptionStatus;
|
||||
|
||||
if (this.organizationId) {
|
||||
const organizationSubscriptionPromise = this.organizationApiService.getSubscription(
|
||||
this.organizationId,
|
||||
);
|
||||
const userId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const organizationPromise = await firstValueFrom(
|
||||
this.organizationService
|
||||
.organizations$(userId)
|
||||
.pipe(getOrganizationById(this.organizationId)),
|
||||
);
|
||||
if (this.organizationId) {
|
||||
const organizationSubscriptionPromise = this.organizationApiService.getSubscription(
|
||||
this.organizationId,
|
||||
);
|
||||
const userId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const organizationPromise = await firstValueFrom(
|
||||
this.organizationService
|
||||
.organizations$(userId)
|
||||
.pipe(getOrganizationById(this.organizationId)),
|
||||
);
|
||||
|
||||
[this.organizationSubscriptionResponse, this.organization] = await Promise.all([
|
||||
organizationSubscriptionPromise,
|
||||
organizationPromise,
|
||||
]);
|
||||
this.freeTrialData = this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(
|
||||
this.organization,
|
||||
this.organizationSubscriptionResponse,
|
||||
paymentSource,
|
||||
);
|
||||
[this.organizationSubscriptionResponse, this.organization] = await Promise.all([
|
||||
organizationSubscriptionPromise,
|
||||
organizationPromise,
|
||||
]);
|
||||
this.freeTrialData = this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(
|
||||
this.organization,
|
||||
this.organizationSubscriptionResponse,
|
||||
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> => {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@NgModule({})
|
||||
import { BillingNotificationService } from "./billing-notification.service";
|
||||
|
||||
@NgModule({
|
||||
providers: [BillingNotificationService],
|
||||
})
|
||||
export class BillingServicesModule {}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
@@ -75,7 +76,7 @@ export class MigrateFromLegacyEncryptionComponent {
|
||||
} catch (e) {
|
||||
// If the error is due to missing folders, we can delete all folders and try again
|
||||
if (
|
||||
e instanceof Error &&
|
||||
e instanceof ErrorResponse &&
|
||||
e.message === "All existing folders must be included in the rotation."
|
||||
) {
|
||||
const deleteFolders = await this.dialogService.openSimpleDialog({
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
take,
|
||||
takeUntil,
|
||||
tap,
|
||||
catchError,
|
||||
} from "rxjs/operators";
|
||||
|
||||
import {
|
||||
@@ -80,6 +81,7 @@ import {
|
||||
CollectionDialogTabType,
|
||||
openCollectionDialog,
|
||||
} 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 { FreeTrial } from "../../billing/types/free-trial";
|
||||
import { SharedModule } from "../../shared/shared.module";
|
||||
@@ -213,20 +215,25 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
ownerOrgs.map((org) =>
|
||||
combineLatest([
|
||||
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(
|
||||
map(([subscription, paymentSource]) => {
|
||||
return this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(
|
||||
map(([subscription, paymentSource]) =>
|
||||
this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(
|
||||
org,
|
||||
subscription,
|
||||
paymentSource,
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
map((results) => results.filter((result) => result.shownBanner)),
|
||||
map((results) => results.filter((result) => result !== null && result.shownBanner)),
|
||||
shareReplay({ refCount: false, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
@@ -262,6 +269,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
protected billingApiService: BillingApiServiceAbstraction,
|
||||
private trialFlowService: TrialFlowService,
|
||||
private organizationBillingService: OrganizationBillingServiceAbstraction,
|
||||
private billingNotificationService: BillingNotificationService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
|
||||
@@ -81,6 +81,7 @@ const moduleRules = [
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
configFile: "../../babel.config.json",
|
||||
cacheDirectory: NODE_ENV !== "production",
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -349,6 +350,20 @@ const webpackConfig = {
|
||||
styles: ["./src/scss/styles.scss", "./src/scss/tailwind.css"],
|
||||
theme_head: "./src/theme.ts",
|
||||
},
|
||||
cache:
|
||||
NODE_ENV === "production"
|
||||
? false
|
||||
: {
|
||||
type: "filesystem",
|
||||
allowCollectingMemory: true,
|
||||
cacheDirectory: path.resolve(__dirname, "../../node_modules/.cache/webpack"),
|
||||
buildDependencies: {
|
||||
config: [__filename],
|
||||
},
|
||||
},
|
||||
snapshot: {
|
||||
unmanagedPaths: [path.resolve(__dirname, "../../node_modules/@bitwarden/")],
|
||||
},
|
||||
optimization: {
|
||||
splitChunks: {
|
||||
cacheGroups: {
|
||||
@@ -361,6 +376,7 @@ const webpackConfig = {
|
||||
},
|
||||
},
|
||||
},
|
||||
minimize: NODE_ENV === "production",
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
terserOptions: {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { map } from "rxjs";
|
||||
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
import { InvoiceResponse } from "@bitwarden/common/billing/models/response/invoices.response";
|
||||
import { BillingNotificationService } from "@bitwarden/web-vault/app/billing/services/billing-notification.service";
|
||||
|
||||
@Component({
|
||||
templateUrl: "./provider-billing-history.component.html",
|
||||
@@ -19,6 +20,7 @@ export class ProviderBillingHistoryComponent {
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private billingApiService: BillingApiServiceAbstraction,
|
||||
private datePipe: DatePipe,
|
||||
private billingNotificationService: BillingNotificationService,
|
||||
) {
|
||||
this.activatedRoute.params
|
||||
.pipe(
|
||||
@@ -30,13 +32,27 @@ export class ProviderBillingHistoryComponent {
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
getClientInvoiceReport = (invoiceId: string) =>
|
||||
this.billingApiService.getProviderClientInvoiceReport(this.providerId, invoiceId);
|
||||
getClientInvoiceReport = async (invoiceId: string) => {
|
||||
try {
|
||||
return await this.billingApiService.getProviderClientInvoiceReport(
|
||||
this.providerId,
|
||||
invoiceId,
|
||||
);
|
||||
} catch (error) {
|
||||
this.billingNotificationService.handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
getClientInvoiceReportName = (invoice: InvoiceResponse) => {
|
||||
const date = this.datePipe.transform(invoice.date, "yyyyMMdd");
|
||||
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 { ProviderPlanResponse } from "@bitwarden/common/billing/models/response/provider-subscription-response";
|
||||
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 = {
|
||||
organization: ProviderOrganizationOrganizationDetailsResponse;
|
||||
@@ -56,30 +57,34 @@ export class ManageClientSubscriptionDialogComponent implements OnInit {
|
||||
@Inject(DIALOG_DATA) protected dialogParams: ManageClientSubscriptionDialogParams,
|
||||
private dialogRef: DialogRef<ManageClientSubscriptionDialogResultType>,
|
||||
private i18nService: I18nService,
|
||||
private toastService: ToastService,
|
||||
private billingNotificationService: BillingNotificationService,
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
const response = await this.billingApiService.getProviderSubscription(
|
||||
this.dialogParams.provider.id,
|
||||
);
|
||||
try {
|
||||
const response = await this.billingApiService.getProviderSubscription(
|
||||
this.dialogParams.provider.id,
|
||||
);
|
||||
|
||||
this.providerPlan = response.plans.find(
|
||||
(plan) => plan.planName === this.dialogParams.organization.plan,
|
||||
);
|
||||
this.providerPlan = response.plans.find(
|
||||
(plan) => plan.planName === this.dialogParams.organization.plan,
|
||||
);
|
||||
|
||||
this.assignedSeats = this.providerPlan.assignedSeats;
|
||||
this.openSeats = this.providerPlan.seatMinimum - this.providerPlan.assignedSeats;
|
||||
this.purchasedSeats = this.providerPlan.purchasedSeats;
|
||||
this.seatMinimum = this.providerPlan.seatMinimum;
|
||||
this.assignedSeats = this.providerPlan.assignedSeats;
|
||||
this.openSeats = this.providerPlan.seatMinimum - this.providerPlan.assignedSeats;
|
||||
this.purchasedSeats = this.providerPlan.purchasedSeats;
|
||||
this.seatMinimum = this.providerPlan.seatMinimum;
|
||||
|
||||
this.formGroup.controls.assignedSeats.addValidators(
|
||||
this.isServiceUserWithPurchasedSeats
|
||||
? this.createPurchasedSeatsValidator()
|
||||
: this.createUnassignedSeatsValidator(),
|
||||
);
|
||||
|
||||
this.loading = false;
|
||||
this.formGroup.controls.assignedSeats.addValidators(
|
||||
this.isServiceUserWithPurchasedSeats
|
||||
? this.createPurchasedSeatsValidator()
|
||||
: this.createUnassignedSeatsValidator(),
|
||||
);
|
||||
} catch (error) {
|
||||
this.billingNotificationService.handleError(error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
@@ -91,24 +96,25 @@ export class ManageClientSubscriptionDialogComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
const request = new UpdateClientOrganizationRequest();
|
||||
request.assignedSeats = this.formGroup.value.assignedSeats;
|
||||
request.name = this.dialogParams.organization.organizationName;
|
||||
try {
|
||||
const request = new UpdateClientOrganizationRequest();
|
||||
request.assignedSeats = this.formGroup.value.assignedSeats;
|
||||
request.name = this.dialogParams.organization.organizationName;
|
||||
|
||||
await this.billingApiService.updateProviderClientOrganization(
|
||||
this.dialogParams.provider.id,
|
||||
this.dialogParams.organization.id,
|
||||
request,
|
||||
);
|
||||
await this.billingApiService.updateProviderClientOrganization(
|
||||
this.dialogParams.provider.id,
|
||||
this.dialogParams.organization.id,
|
||||
request,
|
||||
);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("subscriptionUpdated"),
|
||||
});
|
||||
this.billingNotificationService.showSuccess(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 =
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
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 { WebProviderService } from "../../../admin-console/providers/services/web-provider.service";
|
||||
@@ -83,6 +84,7 @@ export class ManageClientsComponent {
|
||||
private validationService: ValidationService,
|
||||
private webProviderService: WebProviderService,
|
||||
private configService: ConfigService,
|
||||
private billingNotificationService: BillingNotificationService,
|
||||
) {
|
||||
this.activatedRoute.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((queryParams) => {
|
||||
this.searchControl.setValue(queryParams.search);
|
||||
@@ -120,13 +122,17 @@ export class ManageClientsComponent {
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.provider = await firstValueFrom(this.providerService.get$(this.providerId));
|
||||
this.isProviderAdmin = this.provider?.type === ProviderUserType.ProviderAdmin;
|
||||
this.dataSource.data = (
|
||||
await this.billingApiService.getProviderClientOrganizations(this.providerId)
|
||||
).data;
|
||||
this.plans = (await this.billingApiService.getPlans()).data;
|
||||
this.loading = false;
|
||||
try {
|
||||
this.provider = await firstValueFrom(this.providerService.get$(this.providerId));
|
||||
this.isProviderAdmin = this.provider?.type === ProviderUserType.ProviderAdmin;
|
||||
this.dataSource.data = (
|
||||
await this.billingApiService.getProviderClientOrganizations(this.providerId)
|
||||
).data;
|
||||
this.plans = (await this.billingApiService.getPlans()).data;
|
||||
this.loading = false;
|
||||
} catch (error) {
|
||||
this.billingNotificationService.handleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
addExistingOrganization = async () => {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
ProviderSubscriptionResponse,
|
||||
} from "@bitwarden/common/billing/models/response/provider-subscription-response";
|
||||
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({
|
||||
selector: "app-provider-subscription",
|
||||
@@ -33,7 +33,7 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy {
|
||||
private billingApiService: BillingApiServiceAbstraction,
|
||||
private i18nService: I18nService,
|
||||
private route: ActivatedRoute,
|
||||
private toastService: ToastService,
|
||||
private billingNotificationService: BillingNotificationService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -54,20 +54,26 @@ export class ProviderSubscriptionComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
this.subscription = await this.billingApiService.getProviderSubscription(this.providerId);
|
||||
this.totalCost =
|
||||
((100 - this.subscription.discountPercentage) / 100) * this.sumCost(this.subscription.plans);
|
||||
this.loading = false;
|
||||
try {
|
||||
this.subscription = await this.billingApiService.getProviderSubscription(this.providerId);
|
||||
this.totalCost =
|
||||
((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) => {
|
||||
const request = ExpandedTaxInfoUpdateRequest.From(taxInformation);
|
||||
await this.billingApiService.updateProviderTaxInformation(this.providerId, request);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("updatedTaxInformation"),
|
||||
});
|
||||
try {
|
||||
const request = ExpandedTaxInfoUpdateRequest.From(taxInformation);
|
||||
await this.billingApiService.updateProviderTaxInformation(this.providerId, request);
|
||||
this.billingNotificationService.showSuccess(this.i18nService.t("updatedTaxInformation"));
|
||||
} catch (error) {
|
||||
this.billingNotificationService.handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
protected getFormattedCost(
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
firstValueFrom,
|
||||
of,
|
||||
filter,
|
||||
catchError,
|
||||
from,
|
||||
} from "rxjs";
|
||||
|
||||
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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
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 { FreeTrial } from "@bitwarden/web-vault/app/billing/types/free-trial";
|
||||
|
||||
@@ -126,6 +129,7 @@ export class OverviewComponent implements OnInit, OnDestroy {
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private trialFlowService: TrialFlowService,
|
||||
private organizationBillingService: OrganizationBillingServiceAbstraction,
|
||||
private billingNotificationService: BillingNotificationService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -161,12 +165,18 @@ export class OverviewComponent implements OnInit, OnDestroy {
|
||||
combineLatest([
|
||||
of(org),
|
||||
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]) => {
|
||||
return this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(org, sub, paymentSource);
|
||||
}),
|
||||
filter((result) => result !== null),
|
||||
takeUntil(this.destroy$),
|
||||
);
|
||||
|
||||
|
||||
@@ -1284,7 +1284,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: BillingApiServiceAbstraction,
|
||||
useClass: BillingApiService,
|
||||
deps: [ApiServiceAbstraction, LogService, ToastService],
|
||||
deps: [ApiServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: TaxServiceAbstraction,
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request";
|
||||
import { ProviderOrganizationOrganizationDetailsResponse } from "../../admin-console/models/response/provider/provider-organization.response";
|
||||
import { ErrorResponse } from "../../models/response/error.response";
|
||||
import { ListResponse } from "../../models/response/list.response";
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { BillingApiServiceAbstraction } from "../abstractions";
|
||||
import { PaymentMethodType } from "../enums";
|
||||
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";
|
||||
|
||||
export class BillingApiService implements BillingApiServiceAbstraction {
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private logService: LogService,
|
||||
private toastService: ToastService,
|
||||
) {}
|
||||
constructor(private apiService: ApiService) {}
|
||||
|
||||
cancelOrganizationSubscription(
|
||||
organizationId: string,
|
||||
@@ -89,14 +82,12 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
||||
}
|
||||
|
||||
async getOrganizationPaymentMethod(organizationId: string): Promise<PaymentMethodResponse> {
|
||||
const response = await this.execute(() =>
|
||||
this.apiService.send(
|
||||
"GET",
|
||||
"/organizations/" + organizationId + "/billing/payment-method",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
const response = await this.apiService.send(
|
||||
"GET",
|
||||
"/organizations/" + organizationId + "/billing/payment-method",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new PaymentMethodResponse(response);
|
||||
}
|
||||
@@ -120,34 +111,34 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
||||
async getProviderClientOrganizations(
|
||||
providerId: string,
|
||||
): Promise<ListResponse<ProviderOrganizationOrganizationDetailsResponse>> {
|
||||
const response = await this.execute(() =>
|
||||
this.apiService.send("GET", "/providers/" + providerId + "/organizations", null, true, true),
|
||||
const response = await this.apiService.send(
|
||||
"GET",
|
||||
"/providers/" + providerId + "/organizations",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new ListResponse(response, ProviderOrganizationOrganizationDetailsResponse);
|
||||
}
|
||||
|
||||
async getProviderInvoices(providerId: string): Promise<InvoicesResponse> {
|
||||
const response = await this.execute(() =>
|
||||
this.apiService.send(
|
||||
"GET",
|
||||
"/providers/" + providerId + "/billing/invoices",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
const response = await this.apiService.send(
|
||||
"GET",
|
||||
"/providers/" + providerId + "/billing/invoices",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new InvoicesResponse(response);
|
||||
}
|
||||
|
||||
async getProviderSubscription(providerId: string): Promise<ProviderSubscriptionResponse> {
|
||||
const response = await this.execute(() =>
|
||||
this.apiService.send(
|
||||
"GET",
|
||||
"/providers/" + providerId + "/billing/subscription",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
const response = await this.apiService.send(
|
||||
"GET",
|
||||
"/providers/" + providerId + "/billing/subscription",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new ProviderSubscriptionResponse(response);
|
||||
}
|
||||
@@ -227,20 +218,4 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ export enum FeatureFlag {
|
||||
IdpAutoSubmitLogin = "idp-auto-submit-login",
|
||||
InlineMenuFieldQualification = "inline-menu-field-qualification",
|
||||
InlineMenuPositioningImprovements = "inline-menu-positioning-improvements",
|
||||
InlineMenuTotp = "inline-menu-totp",
|
||||
NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements",
|
||||
NotificationRefresh = "notification-refresh",
|
||||
UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection",
|
||||
@@ -74,7 +73,6 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.IdpAutoSubmitLogin]: FALSE,
|
||||
[FeatureFlag.InlineMenuFieldQualification]: FALSE,
|
||||
[FeatureFlag.InlineMenuPositioningImprovements]: FALSE,
|
||||
[FeatureFlag.InlineMenuTotp]: FALSE,
|
||||
[FeatureFlag.NotificationBarAddLoginImprovements]: FALSE,
|
||||
[FeatureFlag.NotificationRefresh]: FALSE,
|
||||
[FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE,
|
||||
|
||||
@@ -6,13 +6,8 @@
|
||||
"@bitwarden/auth/common": ["../auth/src/common"],
|
||||
// TODO: Remove once circular dependencies in admin-console, auth and key-management are resolved
|
||||
"@bitwarden/common/*": ["../common/src/*"],
|
||||
// TODO: Remove once billing stops depending on components
|
||||
"@bitwarden/components": ["../components/src"],
|
||||
"@bitwarden/key-management": ["../key-management/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"]
|
||||
"@bitwarden/vault-export-core": ["../tools/export/vault-export/vault-export-core/src"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "spec", "./custom-matchers.d.ts", "../key-management/src/index.ts"],
|
||||
|
||||
Reference in New Issue
Block a user