1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-14 15:23:33 +00: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:
Alex Morask
2025-07-28 09:26:19 -05:00
committed by GitHub
parent 38d5edc2c5
commit f4254ba920
10 changed files with 518 additions and 64 deletions

View File

@@ -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,
);
}

View File

@@ -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";

View File

@@ -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,
});
}

View File

@@ -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;
}
}
};
}

View File

@@ -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": {