mirror of
https://github.com/bitwarden/clients.git
synced 2025-12-10 10:27:10 -06:00
[PM-21821] Provider portal takeover states (#15725)
* Updates: - Update simple dialog to disallow user to close the dialog on acceptance - Split payment components to provide a "require" component that cannot be closed out of - Add provider warning service to manage the various provider warnings * Fix test * Will's feedback and sync on payment method success
This commit is contained in:
parent
38d5edc2c5
commit
f4254ba920
@ -1,5 +1,5 @@
|
||||
import { DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { Component, Inject, ViewChild } from "@angular/core";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DialogConfig, DialogRef, DialogService, ToastService } from "@bitwarden/components";
|
||||
@ -7,19 +7,17 @@ import { DialogConfig, DialogRef, DialogService, ToastService } from "@bitwarden
|
||||
import { SharedModule } from "../../../shared";
|
||||
import { BillingClient } from "../../services";
|
||||
import { BillableEntity } from "../../types";
|
||||
import { MaskedPaymentMethod } from "../types";
|
||||
|
||||
import { EnterPaymentMethodComponent } from "./enter-payment-method.component";
|
||||
import {
|
||||
SubmitPaymentMethodDialogComponent,
|
||||
SubmitPaymentMethodDialogResult,
|
||||
} from "./submit-payment-method-dialog.component";
|
||||
|
||||
type DialogParams = {
|
||||
owner: BillableEntity;
|
||||
};
|
||||
|
||||
type DialogResult =
|
||||
| { type: "cancelled" }
|
||||
| { type: "error" }
|
||||
| { type: "success"; paymentMethod: MaskedPaymentMethod };
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
@ -55,63 +53,23 @@ type DialogResult =
|
||||
imports: [EnterPaymentMethodComponent, SharedModule],
|
||||
providers: [BillingClient],
|
||||
})
|
||||
export class ChangePaymentMethodDialogComponent {
|
||||
@ViewChild(EnterPaymentMethodComponent)
|
||||
private enterPaymentMethodComponent!: EnterPaymentMethodComponent;
|
||||
protected formGroup = EnterPaymentMethodComponent.getFormGroup();
|
||||
export class ChangePaymentMethodDialogComponent extends SubmitPaymentMethodDialogComponent {
|
||||
protected override owner: BillableEntity;
|
||||
|
||||
constructor(
|
||||
private billingClient: BillingClient,
|
||||
billingClient: BillingClient,
|
||||
@Inject(DIALOG_DATA) protected dialogParams: DialogParams,
|
||||
private dialogRef: DialogRef<DialogResult>,
|
||||
private i18nService: I18nService,
|
||||
private toastService: ToastService,
|
||||
) {}
|
||||
|
||||
submit = async () => {
|
||||
this.formGroup.markAllAsTouched();
|
||||
|
||||
if (!this.formGroup.valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const paymentMethod = await this.enterPaymentMethodComponent.tokenize();
|
||||
const billingAddress =
|
||||
this.formGroup.value.type !== "payPal"
|
||||
? this.formGroup.controls.billingAddress.getRawValue()
|
||||
: null;
|
||||
|
||||
const result = await this.billingClient.updatePaymentMethod(
|
||||
this.dialogParams.owner,
|
||||
paymentMethod,
|
||||
billingAddress,
|
||||
);
|
||||
|
||||
switch (result.type) {
|
||||
case "success": {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("paymentMethodUpdated"),
|
||||
});
|
||||
this.dialogRef.close({
|
||||
type: "success",
|
||||
paymentMethod: result.value,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "error": {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: "",
|
||||
message: result.message,
|
||||
});
|
||||
this.dialogRef.close({ type: "error" });
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
dialogRef: DialogRef<SubmitPaymentMethodDialogResult>,
|
||||
i18nService: I18nService,
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(billingClient, dialogRef, i18nService, toastService);
|
||||
this.owner = this.dialogParams.owner;
|
||||
}
|
||||
|
||||
static open = (dialogService: DialogService, dialogConfig: DialogConfig<DialogParams>) =>
|
||||
dialogService.open<DialogResult>(ChangePaymentMethodDialogComponent, dialogConfig);
|
||||
dialogService.open<SubmitPaymentMethodDialogResult>(
|
||||
ChangePaymentMethodDialogComponent,
|
||||
dialogConfig,
|
||||
);
|
||||
}
|
||||
|
||||
@ -6,4 +6,6 @@ export * from "./display-payment-method.component";
|
||||
export * from "./edit-billing-address-dialog.component";
|
||||
export * from "./enter-billing-address.component";
|
||||
export * from "./enter-payment-method.component";
|
||||
export * from "./require-payment-method-dialog.component";
|
||||
export * from "./submit-payment-method-dialog.component";
|
||||
export * from "./verify-bank-account.component";
|
||||
|
||||
@ -0,0 +1,77 @@
|
||||
import { DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import {
|
||||
CalloutTypes,
|
||||
DialogConfig,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { SharedModule } from "../../../shared";
|
||||
import { BillingClient } from "../../services";
|
||||
import { BillableEntity } from "../../types";
|
||||
|
||||
import { EnterPaymentMethodComponent } from "./enter-payment-method.component";
|
||||
import {
|
||||
SubmitPaymentMethodDialogComponent,
|
||||
SubmitPaymentMethodDialogResult,
|
||||
} from "./submit-payment-method-dialog.component";
|
||||
|
||||
type DialogParams = {
|
||||
owner: BillableEntity;
|
||||
callout: {
|
||||
type: CalloutTypes;
|
||||
title: string;
|
||||
message: string;
|
||||
};
|
||||
};
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog>
|
||||
<span bitDialogTitle class="tw-font-semibold">
|
||||
{{ "addPaymentMethod" | i18n }}
|
||||
</span>
|
||||
<div bitDialogContent>
|
||||
<bit-callout [type]="dialogParams.callout.type" [title]="dialogParams.callout.title">
|
||||
{{ dialogParams.callout.message }}
|
||||
</bit-callout>
|
||||
<app-enter-payment-method [group]="formGroup" [includeBillingAddress]="true">
|
||||
</app-enter-payment-method>
|
||||
</div>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton bitFormButton buttonType="primary" type="submit">
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
`,
|
||||
standalone: true,
|
||||
imports: [EnterPaymentMethodComponent, SharedModule],
|
||||
providers: [BillingClient],
|
||||
})
|
||||
export class RequirePaymentMethodDialogComponent extends SubmitPaymentMethodDialogComponent {
|
||||
protected override owner: BillableEntity;
|
||||
|
||||
constructor(
|
||||
billingClient: BillingClient,
|
||||
@Inject(DIALOG_DATA) protected dialogParams: DialogParams,
|
||||
dialogRef: DialogRef<SubmitPaymentMethodDialogResult>,
|
||||
i18nService: I18nService,
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(billingClient, dialogRef, i18nService, toastService);
|
||||
this.owner = this.dialogParams.owner;
|
||||
}
|
||||
|
||||
static open = (dialogService: DialogService, dialogConfig: DialogConfig<DialogParams>) =>
|
||||
dialogService.open<SubmitPaymentMethodDialogResult>(RequirePaymentMethodDialogComponent, {
|
||||
...dialogConfig,
|
||||
disableClose: true,
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
import { Component, ViewChild } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DialogRef, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { BillingClient } from "../../services";
|
||||
import { BillableEntity } from "../../types";
|
||||
import { MaskedPaymentMethod } from "../types";
|
||||
|
||||
import { EnterPaymentMethodComponent } from "./enter-payment-method.component";
|
||||
|
||||
export type SubmitPaymentMethodDialogResult =
|
||||
| { type: "cancelled" }
|
||||
| { type: "error" }
|
||||
| { type: "success"; paymentMethod: MaskedPaymentMethod };
|
||||
|
||||
@Component({ template: "" })
|
||||
export abstract class SubmitPaymentMethodDialogComponent {
|
||||
@ViewChild(EnterPaymentMethodComponent)
|
||||
private enterPaymentMethodComponent!: EnterPaymentMethodComponent;
|
||||
protected formGroup = EnterPaymentMethodComponent.getFormGroup();
|
||||
|
||||
protected abstract owner: BillableEntity;
|
||||
|
||||
protected constructor(
|
||||
protected billingClient: BillingClient,
|
||||
protected dialogRef: DialogRef<SubmitPaymentMethodDialogResult>,
|
||||
protected i18nService: I18nService,
|
||||
protected toastService: ToastService,
|
||||
) {}
|
||||
|
||||
submit = async () => {
|
||||
this.formGroup.markAllAsTouched();
|
||||
|
||||
if (!this.formGroup.valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const paymentMethod = await this.enterPaymentMethodComponent.tokenize();
|
||||
const billingAddress =
|
||||
this.formGroup.value.type !== "payPal"
|
||||
? this.formGroup.controls.billingAddress.getRawValue()
|
||||
: null;
|
||||
|
||||
const result = await this.billingClient.updatePaymentMethod(
|
||||
this.owner,
|
||||
paymentMethod,
|
||||
billingAddress,
|
||||
);
|
||||
|
||||
switch (result.type) {
|
||||
case "success": {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("paymentMethodUpdated"),
|
||||
});
|
||||
this.dialogRef.close({
|
||||
type: "success",
|
||||
paymentMethod: result.value,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "error": {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: "",
|
||||
message: result.message,
|
||||
});
|
||||
this.dialogRef.close({ type: "error" });
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -10922,6 +10922,36 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"unpaidInvoices": {
|
||||
"message": "Unpaid invoices"
|
||||
},
|
||||
"unpaidInvoicesForServiceUser": {
|
||||
"message": "Your subscription has not been paid. Contact your provider administrator to restore service to you and your clients.",
|
||||
"description": "A message shown in a non-dismissible dialog to service users of unpaid providers."
|
||||
},
|
||||
"providerSuspended": {
|
||||
"message": "$PROVIDER$ is suspended",
|
||||
"placeholders": {
|
||||
"provider": {
|
||||
"content": "$1",
|
||||
"example": "Acme Industries"
|
||||
}
|
||||
}
|
||||
},
|
||||
"restoreProviderPortalAccessViaCustomerSupport": {
|
||||
"message": "To restore access to your provider portal, contact Bitwarden Customer Support to renew your subscription.",
|
||||
"description": "A message shown in a non-dismissible dialog to any user of a suspended providers."
|
||||
},
|
||||
"restoreProviderPortalAccessViaPaymentMethod": {
|
||||
"message": "Your subscription has not been paid. To restore service to you and your clients, add a payment method by $CANCELLATION_DATE$.",
|
||||
"placeholders": {
|
||||
"cancellation_date": {
|
||||
"content": "$1",
|
||||
"example": "07/10/2025"
|
||||
}
|
||||
},
|
||||
"description": "A message shown in a non-dismissible dialog to admins of unpaid providers."
|
||||
},
|
||||
"subscribetoEnterprise": {
|
||||
"message": "Subscribe to $PLAN$",
|
||||
"placeholders": {
|
||||
|
||||
@ -17,10 +17,13 @@ import { BusinessUnitPortalLogo } from "@bitwarden/web-vault/app/admin-console/i
|
||||
import { ProviderPortalLogo } from "@bitwarden/web-vault/app/admin-console/icons/provider-portal-logo";
|
||||
import { WebLayoutModule } from "@bitwarden/web-vault/app/layouts/web-layout.module";
|
||||
|
||||
import { ProviderWarningsService } from "../../billing/providers/services/provider-warnings.service";
|
||||
|
||||
@Component({
|
||||
selector: "providers-layout",
|
||||
templateUrl: "providers-layout.component.html",
|
||||
imports: [CommonModule, RouterModule, JslibModule, WebLayoutModule, IconModule],
|
||||
providers: [ProviderWarningsService],
|
||||
})
|
||||
export class ProvidersLayoutComponent implements OnInit, OnDestroy {
|
||||
protected readonly logo = ProviderPortalLogo;
|
||||
@ -40,13 +43,18 @@ export class ProvidersLayoutComponent implements OnInit, OnDestroy {
|
||||
private route: ActivatedRoute,
|
||||
private providerService: ProviderService,
|
||||
private configService: ConfigService,
|
||||
private providerWarningsService: ProviderWarningsService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
document.body.classList.remove("layout_frontend");
|
||||
|
||||
this.provider$ = this.route.params.pipe(
|
||||
switchMap((params) => this.providerService.get$(params.providerId)),
|
||||
const providerId$: Observable<string> = this.route.params.pipe(
|
||||
map((params) => params.providerId),
|
||||
);
|
||||
|
||||
this.provider$ = providerId$.pipe(
|
||||
switchMap((providerId) => this.providerService.get$(providerId)),
|
||||
takeUntil(this.destroy$),
|
||||
);
|
||||
|
||||
@ -77,6 +85,15 @@ export class ProvidersLayoutComponent implements OnInit, OnDestroy {
|
||||
this.managePaymentDetailsOutsideCheckout$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout,
|
||||
);
|
||||
|
||||
providerId$
|
||||
.pipe(
|
||||
switchMap((providerId) =>
|
||||
this.providerWarningsService.showProviderSuspendedDialog$(providerId),
|
||||
),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
||||
@ -0,0 +1,187 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||
import { ProviderUserType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
import { ProviderSubscriptionResponse } from "@bitwarden/common/billing/models/response/provider-subscription-response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { DialogRef, DialogService } from "@bitwarden/components";
|
||||
import {
|
||||
RequirePaymentMethodDialogComponent,
|
||||
SubmitPaymentMethodDialogResult,
|
||||
} from "@bitwarden/web-vault/app/billing/payment/components";
|
||||
|
||||
import { ProviderWarningsService } from "./provider-warnings.service";
|
||||
|
||||
describe("ProviderWarningsService", () => {
|
||||
let service: ProviderWarningsService;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
let dialogService: MockProxy<DialogService>;
|
||||
let providerService: MockProxy<ProviderService>;
|
||||
let billingApiService: MockProxy<BillingApiServiceAbstraction>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let router: MockProxy<Router>;
|
||||
let syncService: MockProxy<SyncService>;
|
||||
|
||||
beforeEach(() => {
|
||||
billingApiService = mock<BillingApiServiceAbstraction>();
|
||||
configService = mock<ConfigService>();
|
||||
dialogService = mock<DialogService>();
|
||||
i18nService = mock<I18nService>();
|
||||
providerService = mock<ProviderService>();
|
||||
router = mock<Router>();
|
||||
syncService = mock<SyncService>();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
ProviderWarningsService,
|
||||
{ provide: ActivatedRoute, useValue: {} },
|
||||
{ provide: BillingApiServiceAbstraction, useValue: billingApiService },
|
||||
{ provide: ConfigService, useValue: configService },
|
||||
{ provide: DialogService, useValue: dialogService },
|
||||
{ provide: I18nService, useValue: i18nService },
|
||||
{ provide: ProviderService, useValue: providerService },
|
||||
{ provide: Router, useValue: router },
|
||||
{ provide: SyncService, useValue: syncService },
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(ProviderWarningsService);
|
||||
});
|
||||
|
||||
it("should create the service", () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("showProviderSuspendedDialog$", () => {
|
||||
const providerId = "test-provider-id";
|
||||
|
||||
it("should not show any dialog when the 'pm-21821-provider-portal-takeover' flag is disabled", (done) => {
|
||||
const provider = { enabled: false } as Provider;
|
||||
const subscription = { status: "unpaid" } as ProviderSubscriptionResponse;
|
||||
|
||||
providerService.get$.mockReturnValue(of(provider));
|
||||
billingApiService.getProviderSubscription.mockResolvedValue(subscription);
|
||||
configService.getFeatureFlag$.mockReturnValue(of(false));
|
||||
|
||||
const requirePaymentMethodDialogComponentOpenSpy = jest.spyOn(
|
||||
RequirePaymentMethodDialogComponent,
|
||||
"open",
|
||||
);
|
||||
|
||||
service.showProviderSuspendedDialog$(providerId).subscribe(() => {
|
||||
expect(dialogService.openSimpleDialog).not.toHaveBeenCalled();
|
||||
expect(requirePaymentMethodDialogComponentOpenSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not show any dialog when the provider is enabled", (done) => {
|
||||
const provider = { enabled: true } as Provider;
|
||||
const subscription = { status: "unpaid" } as ProviderSubscriptionResponse;
|
||||
|
||||
providerService.get$.mockReturnValue(of(provider));
|
||||
billingApiService.getProviderSubscription.mockResolvedValue(subscription);
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
|
||||
const requirePaymentMethodDialogComponentOpenSpy = jest.spyOn(
|
||||
RequirePaymentMethodDialogComponent,
|
||||
"open",
|
||||
);
|
||||
|
||||
service.showProviderSuspendedDialog$(providerId).subscribe(() => {
|
||||
expect(dialogService.openSimpleDialog).not.toHaveBeenCalled();
|
||||
expect(requirePaymentMethodDialogComponentOpenSpy).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should show the require payment method dialog for an admin of a provider with an unpaid subscription", (done) => {
|
||||
const provider = {
|
||||
enabled: false,
|
||||
type: ProviderUserType.ProviderAdmin,
|
||||
name: "Test Provider",
|
||||
} as Provider;
|
||||
const subscription = {
|
||||
status: "unpaid",
|
||||
cancelAt: "2024-12-31",
|
||||
} as ProviderSubscriptionResponse;
|
||||
|
||||
providerService.get$.mockReturnValue(of(provider));
|
||||
billingApiService.getProviderSubscription.mockResolvedValue(subscription);
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
|
||||
const dialogRef = {
|
||||
closed: of({ type: "success" }),
|
||||
} as DialogRef<SubmitPaymentMethodDialogResult>;
|
||||
jest.spyOn(RequirePaymentMethodDialogComponent, "open").mockReturnValue(dialogRef);
|
||||
|
||||
service.showProviderSuspendedDialog$(providerId).subscribe(() => {
|
||||
expect(RequirePaymentMethodDialogComponent.open).toHaveBeenCalled();
|
||||
expect(syncService.fullSync).toHaveBeenCalled();
|
||||
expect(router.navigate).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should show the simple, unpaid invoices dialog for a service user of a provider with an unpaid subscription", (done) => {
|
||||
const provider = {
|
||||
enabled: false,
|
||||
type: ProviderUserType.ServiceUser,
|
||||
name: "Test Provider",
|
||||
} as Provider;
|
||||
const subscription = { status: "unpaid" } as ProviderSubscriptionResponse;
|
||||
|
||||
providerService.get$.mockReturnValue(of(provider));
|
||||
billingApiService.getProviderSubscription.mockResolvedValue(subscription);
|
||||
dialogService.openSimpleDialog.mockResolvedValue(true);
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
|
||||
i18nService.t.mockImplementation((key: string) => key);
|
||||
|
||||
service.showProviderSuspendedDialog$(providerId).subscribe(() => {
|
||||
expect(dialogService.openSimpleDialog).toHaveBeenCalledWith({
|
||||
type: "danger",
|
||||
title: "unpaidInvoices",
|
||||
content: "unpaidInvoicesForServiceUser",
|
||||
disableClose: true,
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should show the provider suspended dialog to all users of a provider that's suspended, but not unpaid", (done) => {
|
||||
const provider = {
|
||||
enabled: false,
|
||||
name: "Test Provider",
|
||||
} as Provider;
|
||||
const subscription = { status: "active" } as ProviderSubscriptionResponse;
|
||||
|
||||
providerService.get$.mockReturnValue(of(provider));
|
||||
billingApiService.getProviderSubscription.mockResolvedValue(subscription);
|
||||
dialogService.openSimpleDialog.mockResolvedValue(true);
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
|
||||
i18nService.t.mockImplementation((key: string) => key);
|
||||
|
||||
service.showProviderSuspendedDialog$(providerId).subscribe(() => {
|
||||
expect(dialogService.openSimpleDialog).toHaveBeenCalledWith({
|
||||
type: "danger",
|
||||
title: "providerSuspended",
|
||||
content: "restoreProviderPortalAccessViaCustomerSupport",
|
||||
disableClose: true,
|
||||
acceptButtonText: "contactSupportShort",
|
||||
cancelButtonText: null,
|
||||
acceptAction: expect.any(Function),
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,104 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { combineLatest, from, lastValueFrom, Observable, switchMap } from "rxjs";
|
||||
|
||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||
import { ProviderUserType } from "@bitwarden/common/admin-console/enums";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { RequirePaymentMethodDialogComponent } from "@bitwarden/web-vault/app/billing/payment/components";
|
||||
|
||||
@Injectable()
|
||||
export class ProviderWarningsService {
|
||||
constructor(
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private billingApiService: BillingApiServiceAbstraction,
|
||||
private configService: ConfigService,
|
||||
private dialogService: DialogService,
|
||||
private i18nService: I18nService,
|
||||
private providerService: ProviderService,
|
||||
private router: Router,
|
||||
private syncService: SyncService,
|
||||
) {}
|
||||
|
||||
showProviderSuspendedDialog$ = (providerId: string): Observable<void> =>
|
||||
combineLatest([
|
||||
this.configService.getFeatureFlag$(FeatureFlag.PM21821_ProviderPortalTakeover),
|
||||
this.providerService.get$(providerId),
|
||||
from(this.billingApiService.getProviderSubscription(providerId)),
|
||||
]).pipe(
|
||||
switchMap(async ([providerPortalTakeover, provider, subscription]) => {
|
||||
if (!providerPortalTakeover || provider.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (subscription.status === "unpaid") {
|
||||
switch (provider.type) {
|
||||
case ProviderUserType.ProviderAdmin: {
|
||||
const cancelAt = subscription.cancelAt
|
||||
? new Date(subscription.cancelAt).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "2-digit",
|
||||
year: "numeric",
|
||||
})
|
||||
: null;
|
||||
|
||||
const dialogRef = RequirePaymentMethodDialogComponent.open(this.dialogService, {
|
||||
data: {
|
||||
owner: {
|
||||
type: "provider",
|
||||
data: provider,
|
||||
},
|
||||
callout: {
|
||||
type: "danger",
|
||||
title: this.i18nService.t("unpaidInvoices"),
|
||||
message: this.i18nService.t(
|
||||
"restoreProviderPortalAccessViaPaymentMethod",
|
||||
cancelAt ?? undefined,
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
|
||||
if (result?.type === "success") {
|
||||
await this.syncService.fullSync(true);
|
||||
await this.router.navigate(["."], {
|
||||
relativeTo: this.activatedRoute,
|
||||
onSameUrlNavigation: "reload",
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ProviderUserType.ServiceUser: {
|
||||
await this.dialogService.openSimpleDialog({
|
||||
type: "danger",
|
||||
title: this.i18nService.t("unpaidInvoices"),
|
||||
content: this.i18nService.t("unpaidInvoicesForServiceUser"),
|
||||
disableClose: true,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await this.dialogService.openSimpleDialog({
|
||||
type: "danger",
|
||||
title: this.i18nService.t("providerSuspended", provider.name),
|
||||
content: this.i18nService.t("restoreProviderPortalAccessViaCustomerSupport"),
|
||||
disableClose: true,
|
||||
acceptButtonText: this.i18nService.t("contactSupportShort"),
|
||||
cancelButtonText: null,
|
||||
acceptAction: async () => {
|
||||
window.open("https://bitwarden.com/contact/", "_blank");
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
@ -32,6 +32,7 @@ export enum FeatureFlag {
|
||||
UseOrganizationWarningsService = "use-organization-warnings-service",
|
||||
AllowTrialLengthZero = "pm-20322-allow-trial-length-0",
|
||||
PM21881_ManagePaymentDetailsOutsideCheckout = "pm-21881-manage-payment-details-outside-checkout",
|
||||
PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover",
|
||||
|
||||
/* Key Management */
|
||||
PrivateKeyRegeneration = "pm-12241-private-key-regeneration",
|
||||
@ -115,6 +116,7 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.UseOrganizationWarningsService]: FALSE,
|
||||
[FeatureFlag.AllowTrialLengthZero]: FALSE,
|
||||
[FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout]: FALSE,
|
||||
[FeatureFlag.PM21821_ProviderPortalTakeover]: FALSE,
|
||||
|
||||
/* Key Management */
|
||||
[FeatureFlag.PrivateKeyRegeneration]: FALSE,
|
||||
|
||||
@ -68,7 +68,9 @@ export class SimpleConfigurableDialogComponent {
|
||||
await this.simpleDialogOpts.acceptAction();
|
||||
}
|
||||
|
||||
this.dialogRef.close(true);
|
||||
if (!this.simpleDialogOpts.disableClose) {
|
||||
this.dialogRef.close(true);
|
||||
}
|
||||
};
|
||||
|
||||
private localizeText() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user