From 49078203838de8469f94243328f99df455fbd439 Mon Sep 17 00:00:00 2001
From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com>
Date: Tue, 9 Sep 2025 12:22:45 -0500
Subject: [PATCH] [PM-24964] Stripe-hosted bank account verification (#16220)
* Implement bank account hosted URL verification with webhook handling notification
* [PM-25491] Create org/provider bank account warning needs to be updated
---
.../organization-payment-details.component.ts | 31 ++++++++++
.../display-payment-method.component.ts | 24 ++++----
.../enter-payment-method.component.ts | 2 +-
.../payment/types/masked-payment-method.ts | 6 +-
.../shared/payment/payment.component.html | 2 +-
.../shared/payment/payment.component.ts | 8 ---
apps/web/src/locales/en/messages.json | 9 +++
.../provider-payment-details.component.ts | 57 +++++++++++++++++--
.../src/enums/notification-type.enum.ts | 3 +
.../models/response/notification.response.ts | 26 +++++++++
.../default-server-notifications.service.ts | 11 ++++
11 files changed, 149 insertions(+), 30 deletions(-)
diff --git a/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.ts b/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.ts
index d1dfea40fe2..05802d5868a 100644
--- a/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.ts
+++ b/apps/web/src/app/billing/organizations/payment-details/organization-payment-details.component.ts
@@ -19,6 +19,7 @@ import {
take,
takeUntil,
tap,
+ withLatestFrom,
} from "rxjs";
import {
@@ -31,6 +32,7 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { DialogService } from "@bitwarden/components";
+import { CommandDefinition, MessageListener } from "@bitwarden/messaging";
import { SubscriberBillingClient } from "@bitwarden/web-vault/app/billing/clients";
import { OrganizationFreeTrialWarningComponent } from "@bitwarden/web-vault/app/billing/organizations/warnings/components";
import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services";
@@ -67,6 +69,10 @@ type View = {
taxIdWarning: TaxIdWarningType | null;
};
+const BANK_ACCOUNT_VERIFIED_COMMAND = new CommandDefinition<{ organizationId: string }>(
+ "organizationBankAccountVerified",
+);
+
@Component({
templateUrl: "./organization-payment-details.component.html",
standalone: true,
@@ -150,6 +156,7 @@ export class OrganizationPaymentDetailsComponent implements OnInit, OnDestroy {
private activatedRoute: ActivatedRoute,
private configService: ConfigService,
private dialogService: DialogService,
+ private messageListener: MessageListener,
private organizationService: OrganizationService,
private organizationWarningsService: OrganizationWarningsService,
private router: Router,
@@ -195,6 +202,30 @@ export class OrganizationPaymentDetailsComponent implements OnInit, OnDestroy {
}
});
}
+
+ this.messageListener
+ .messages$(BANK_ACCOUNT_VERIFIED_COMMAND)
+ .pipe(
+ withLatestFrom(this.view$),
+ filter(([message, view]) => message.organizationId === view.organization.data.id),
+ switchMap(
+ async ([_, view]) =>
+ await Promise.all([
+ this.subscriberBillingClient.getPaymentMethod(view.organization),
+ this.subscriberBillingClient.getBillingAddress(view.organization),
+ ]),
+ ),
+ tap(async ([paymentMethod, billingAddress]) => {
+ if (paymentMethod) {
+ await this.setPaymentMethod(paymentMethod);
+ }
+ if (billingAddress) {
+ this.setBillingAddress(billingAddress);
+ }
+ }),
+ takeUntil(this.destroy$),
+ )
+ .subscribe();
}
ngOnDestroy() {
diff --git a/apps/web/src/app/billing/payment/components/display-payment-method.component.ts b/apps/web/src/app/billing/payment/components/display-payment-method.component.ts
index df42d04b802..c33d805aed7 100644
--- a/apps/web/src/app/billing/payment/components/display-payment-method.component.ts
+++ b/apps/web/src/app/billing/payment/components/display-payment-method.component.ts
@@ -8,7 +8,6 @@ import { BitwardenSubscriber } from "../../types";
import { MaskedPaymentMethod } from "../types";
import { ChangePaymentMethodDialogComponent } from "./change-payment-method-dialog.component";
-import { VerifyBankAccountComponent } from "./verify-bank-account.component";
@Component({
selector: "app-display-payment-method",
@@ -18,18 +17,23 @@ import { VerifyBankAccountComponent } from "./verify-bank-account.component";
@if (paymentMethod) {
@switch (paymentMethod.type) {
@case ("bankAccount") {
- @if (!paymentMethod.verified) {
-
+ {{ "verifyBankAccountWithStripe" | i18n }} + {{ "verifyNow" | i18n }} +
}{{ paymentMethod.bankName }}, *{{ paymentMethod.last4 }} - @if (!paymentMethod.verified) { + @if (paymentMethod.hostedVerificationUrl) { - {{ "unverified" | i18n }} }
@@ -63,7 +67,7 @@ import { VerifyBankAccountComponent } from "./verify-bank-account.component"; `, standalone: true, - imports: [SharedModule, VerifyBankAccountComponent], + imports: [SharedModule], }) export class DisplayPaymentMethodComponent { @Input({ required: true }) subscriber!: BitwardenSubscriber; @@ -96,8 +100,6 @@ export class DisplayPaymentMethodComponent { } }; - onBankAccountVerified = (paymentMethod: MaskedPaymentMethod) => this.updated.emit(paymentMethod); - protected getBrandIconForCard = (): string | null => { if (this.paymentMethod?.type !== "card") { return null; diff --git a/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts b/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts index b5d732031c0..93c45b873fe 100644 --- a/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts +++ b/apps/web/src/app/billing/payment/components/enter-payment-method.component.ts @@ -118,7 +118,7 @@ type PaymentMethodFormGroup = FormGroup<{ @case ("bankAccount") {