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:
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user