@amos.com/amos-js 0.9.17 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -68,6 +68,13 @@ export declare function updateMerchantName({ iframe, merchantName, }: {
68
68
  export declare function validateForm({ iframe }: {
69
69
  iframe: Iframe;
70
70
  }): Promise<boolean>;
71
+ /**
72
+ * Ask the bank iframe to mint a Plaid Link token (`POST /plaid_link_tokens`
73
+ * on embed). Resolves with `link_token`, or rejects on error / timeout.
74
+ */
75
+ export declare function requestPlaidLinkToken({ iframe, }: {
76
+ iframe: Iframe;
77
+ }): Promise<string>;
71
78
  /**
72
79
  * Clear all field values and API errors in the embedded credit-card or
73
80
  * bank-account iframe form.
package/dist/mount.d.ts CHANGED
@@ -40,6 +40,14 @@ export type AmosPaymentMethodFormMountController = PaymentMethodFormController &
40
40
  */
41
41
  iframe: HTMLIFrameElement;
42
42
  };
43
+ /**
44
+ * Controller returned by {@link mountAmosBankAccountPaymentMethodForm}.
45
+ * `update()` also accepts `amount` (major-currency decimal string) when
46
+ * the charge changes.
47
+ */
48
+ export type AmosBankAccountPaymentMethodFormMountController = Omit<AmosPaymentMethodFormMountController, "update"> & {
49
+ update: (patch: Partial<AmosBankAccountPaymentMethodFormOptions>) => void;
50
+ };
43
51
  /**
44
52
  * Mount the secure credit-card payment method form into a container
45
53
  * element. Returns a controller exposing the underlying iframe, an
@@ -70,20 +78,32 @@ export type AmosBankAccountPaymentMethodFormOptions = PaymentMethodFormListenerO
70
78
  * @default "country"
71
79
  */
72
80
  billingAddressRequirement?: BillingAddressRequirement;
81
+ /**
82
+ * Charge amount as a major-currency decimal string (e.g. `"50.00"`
83
+ * for $50.00), the same format as Google Pay / Apple Pay. Compared
84
+ * to the merchant ACH threshold fetched by the iframe. Omit for
85
+ * setup intents or when the charge is unknown — if a threshold is
86
+ * set, Plaid is required.
87
+ */
88
+ amount?: string;
73
89
  };
74
90
  /**
75
91
  * Mount the secure bank-account payment method form into a container
76
92
  * element. Returns a controller exposing the underlying iframe, an
77
93
  * `update()` method, and a `destroy()` method.
78
94
  *
79
- * A field-shaped skeleton is shown immediately and replaced by the
80
- * iframe once appearance is applied.
95
+ * When the iframe reports an ACH threshold and `amount` meets it (or
96
+ * `amount` is omitted), a Connect bank button is rendered in the parent
97
+ * document and Plaid Link is opened on click. The button uses the same
98
+ * `appearance.themeVariables` as the iframe (and inherits host-page
99
+ * tokens when those variables are unset). Otherwise a field-shaped
100
+ * skeleton is shown and replaced by the iframe once appearance is applied.
81
101
  *
82
102
  * Use the returned `controller.iframe` when calling
83
103
  * {@link validateForm}, {@link confirmPaymentIntent}, or
84
104
  * {@link confirmSetupIntent}.
85
105
  */
86
- export declare function mountAmosBankAccountPaymentMethodForm(container: Container, options: AmosBankAccountPaymentMethodFormOptions): AmosPaymentMethodFormMountController;
106
+ export declare function mountAmosBankAccountPaymentMethodForm(container: Container, options: AmosBankAccountPaymentMethodFormOptions): AmosBankAccountPaymentMethodFormMountController;
87
107
  /**
88
108
  * Options accepted by {@link mountAmosGooglePayButton}.
89
109
  */
@@ -1,4 +1,4 @@
1
- import { Appearance, ConfirmationResult, PaymentMethodFormValidityChangeEvent } from './types';
1
+ import { Appearance, ConfirmationResult, PaymentMethodFormCardBrandChangeEvent, PaymentMethodFormValidityChangeEvent } from './types';
2
2
  /**
3
3
  * The additional fields beyond the standard card number, expiration
4
4
  * date, CVV, and billing address fields that are required to be filled
@@ -65,6 +65,13 @@ export type PaymentMethodFormListenerOptions = {
65
65
  * PCI data. Use this to enable or disable a host checkout button.
66
66
  */
67
67
  onValidityChange?: (event: PaymentMethodFormValidityChangeEvent) => void;
68
+ /**
69
+ * Called when the detected card brand changes. `brand` is the matched
70
+ * network, or `null` when the field is empty or the digits do not
71
+ * match a known brand. Does not include PCI data. Credit-card form
72
+ * only — never fired for bank account.
73
+ */
74
+ onCardBrandChanged?: (event: PaymentMethodFormCardBrandChangeEvent) => void;
68
75
  /**
69
76
  * Called when the interactive confirmation flow finishes (success or
70
77
  * terminal failure). Not settlement proof — verify via webhooks.
@@ -0,0 +1,21 @@
1
+ import { PaymentMethodFormListenerOptions } from './payment-method-form';
2
+ export type PlaidBankUiOptions = {
3
+ amount?: string;
4
+ appearance?: PaymentMethodFormListenerOptions["appearance"];
5
+ onValidityChange?: PaymentMethodFormListenerOptions["onValidityChange"];
6
+ };
7
+ /**
8
+ * Parent-page Connect bank UI. Outline/ghost controls match `@amos/ui`
9
+ * (including `:focus-visible` `--ring`). Unset theme variables inherit
10
+ * from the host document, then fall back to {@link ThemeVariable}
11
+ * defaults; `appearance.themeVariables` overrides with the same replace
12
+ * model as the iframe.
13
+ */
14
+ export declare function attachPlaidBankUi({ host, iframe, options, }: {
15
+ host: HTMLElement;
16
+ iframe: HTMLIFrameElement;
17
+ options: PlaidBankUiOptions;
18
+ }): {
19
+ update: (patch: Partial<PlaidBankUiOptions>) => void;
20
+ destroy: () => void;
21
+ };
@@ -0,0 +1,12 @@
1
+ import { PlaidCredentials } from './plaid';
2
+ type BankPlaidSession = {
3
+ requiresVerification: boolean;
4
+ plaid?: PlaidCredentials;
5
+ /** Drop linked Plaid credentials and restore the Connect button. */
6
+ clearLinked?: () => void;
7
+ };
8
+ export declare function setBankPlaidSession(iframe: HTMLIFrameElement, session: BankPlaidSession): void;
9
+ export declare function getBankPlaidSession(iframe: HTMLIFrameElement | null | undefined): BankPlaidSession | undefined;
10
+ export declare function clearBankPlaidSession(iframe: HTMLIFrameElement | null | undefined): void;
11
+ export declare function getBankPlaidCredentials(iframe: HTMLIFrameElement | null | undefined): PlaidCredentials | undefined;
12
+ export {};
@@ -0,0 +1,82 @@
1
+ import { components } from '@amos.com/node';
2
+ export type PlaidCredentials = components["schemas"]["PlaidCredentialsInput"];
3
+ export type PlaidLinkAccount = {
4
+ id?: string;
5
+ mask?: string;
6
+ name?: string;
7
+ subtype?: string | null;
8
+ type?: string;
9
+ };
10
+ export type PlaidLinkOnSuccessMetadata = {
11
+ institution?: {
12
+ name?: string;
13
+ } | null;
14
+ accounts?: Array<PlaidLinkAccount>;
15
+ account?: PlaidLinkAccount;
16
+ account_id?: string;
17
+ };
18
+ type PlaidLinkHandler = {
19
+ open: () => void;
20
+ exit: (options?: {
21
+ force?: boolean;
22
+ }) => void;
23
+ destroy: () => void;
24
+ };
25
+ type PlaidCreateConfig = {
26
+ token: string;
27
+ onSuccess: (publicToken: string, metadata: PlaidLinkOnSuccessMetadata) => void;
28
+ onExit?: (error: {
29
+ error_code?: string;
30
+ error_message?: string;
31
+ } | null, metadata: unknown) => void;
32
+ };
33
+ declare global {
34
+ interface Window {
35
+ Plaid?: {
36
+ create: (config: PlaidCreateConfig) => PlaidLinkHandler;
37
+ };
38
+ }
39
+ }
40
+ /**
41
+ * Whether the bank form should show Plaid Link instead of routing/account
42
+ * fields.
43
+ *
44
+ * - No `achThreshold`: always manual ACH (backward compatible).
45
+ * - `amount` omitted: Plaid (setup / unknown future charge).
46
+ * - Otherwise: Plaid when `amount >= achThreshold`.
47
+ *
48
+ * `amount` and `achThreshold` are integer minor units (cents).
49
+ */
50
+ export declare function requiresAchVerification({ amount, achThreshold, }: {
51
+ amount?: number;
52
+ achThreshold?: number;
53
+ }): boolean;
54
+ /**
55
+ * Convert a major-currency decimal string (e.g. `"50.00"`) to integer
56
+ * cents. Empty / invalid values are omitted.
57
+ */
58
+ export declare function majorAmountToMinorUnits(amount?: string): number | undefined;
59
+ export declare function plaidAccountIdFromMetadata(metadata: PlaidLinkOnSuccessMetadata): string | undefined;
60
+ export declare function linkedBankLabelFromMetadata(metadata: PlaidLinkOnSuccessMetadata): {
61
+ bankName: string;
62
+ last4: string;
63
+ };
64
+ export declare function loadPlaidScript(): Promise<void>;
65
+ export type OpenPlaidLinkInput = {
66
+ token: string;
67
+ onSuccess: (publicToken: string, metadata: PlaidLinkOnSuccessMetadata) => void;
68
+ onExit?: (error: {
69
+ error_code?: string;
70
+ } | null) => void;
71
+ /**
72
+ * When aborted (e.g. the bank form was unmounted), skip opening Link
73
+ * and destroy the handler if it was already created.
74
+ */
75
+ signal?: AbortSignal;
76
+ };
77
+ /**
78
+ * Load Plaid Link (if needed) and open it with the given `link_token`.
79
+ * Returns a destroy function for the Link handler.
80
+ */
81
+ export declare function openPlaidLink({ token, onSuccess, onExit, signal, }: OpenPlaidLinkInput): Promise<() => void>;
82
+ export {};
package/dist/types.d.ts CHANGED
@@ -7,6 +7,18 @@ import { components } from '@amos.com/node';
7
7
  export type PaymentMethodFormValidityChangeEvent = {
8
8
  isValid: boolean;
9
9
  };
10
+ /**
11
+ * Detected card network from the PAN prefix inside the iframe.
12
+ * `null` when the field is empty or the digits do not match a known brand.
13
+ */
14
+ export type CardBrand = "visa" | "mastercard" | "amex" | "discover" | "diners" | "jcb";
15
+ /**
16
+ * PCI-safe card brand update from the credit-card iframe. Does not
17
+ * include the PAN, last4, or BIN.
18
+ */
19
+ export type PaymentMethodFormCardBrandChangeEvent = {
20
+ brand: CardBrand | null;
21
+ };
10
22
  /**
11
23
  * Why an interactive confirmation attempt ended with `status: "incomplete"`.
12
24
  *
@@ -185,10 +197,16 @@ export type Message = {
185
197
  } | ({
186
198
  /** Parent → embed: confirm a payment intent with an embed token. */
187
199
  type: "CONFIRM_PAYMENT_INTENT";
188
- } & Pick<components["schemas"]["PaymentIntent"], "id"> & Pick<components["schemas"]["EmbedToken"], "token">) | ({
200
+ } & Pick<components["schemas"]["PaymentIntent"], "id"> & Pick<components["schemas"]["EmbedToken"], "token"> & {
201
+ /** Present when ACH verification completed via Plaid Link in the parent. */
202
+ plaid?: components["schemas"]["PlaidCredentialsInput"];
203
+ }) | ({
189
204
  /** Parent → embed: confirm a setup intent with an embed token. */
190
205
  type: "CONFIRM_SETUP_INTENT";
191
- } & Pick<components["schemas"]["SetupIntent"], "id"> & Pick<components["schemas"]["EmbedToken"], "token">) | {
206
+ } & Pick<components["schemas"]["SetupIntent"], "id"> & Pick<components["schemas"]["EmbedToken"], "token"> & {
207
+ /** Present when ACH verification completed via Plaid Link in the parent. */
208
+ plaid?: components["schemas"]["PlaidCredentialsInput"];
209
+ }) | {
192
210
  /**
193
211
  * Embed → parent: the interactive confirmation flow finished.
194
212
  * Parent → embed: express-checkout initiation failed on the host.
@@ -220,6 +238,34 @@ export type Message = {
220
238
  */
221
239
  type: "FORM_VALIDITY_CHANGE";
222
240
  isValid: boolean;
241
+ } | {
242
+ /**
243
+ * Embed → parent: detected card brand changed. `brand` is the
244
+ * matched network, or `null` when the field is empty or the
245
+ * digits do not match a known brand. Does not include PCI data.
246
+ */
247
+ type: "CARD_BRAND_CHANGE";
248
+ brand: CardBrand | null;
249
+ } | {
250
+ /**
251
+ * Embed → parent: merchant ACH verification threshold for this
252
+ * render token. `achThreshold` is cents, or `null` when the
253
+ * merchant has no threshold (manual ACH). `requireVerification`
254
+ * is true when the fetch failed in production (fail closed).
255
+ */
256
+ type: "ACH_THRESHOLD";
257
+ achThreshold?: number | null;
258
+ requireVerification?: boolean;
259
+ } | {
260
+ /** Parent → embed: mint a Plaid Link token for Connect. */
261
+ type: "CREATE_PLAID_LINK_TOKEN";
262
+ requestId: string;
263
+ } | {
264
+ /** Embed → parent: minted Plaid Link token, or `error`. */
265
+ type: "PLAID_LINK_TOKEN";
266
+ requestId: string;
267
+ link_token?: string;
268
+ error?: string;
223
269
  };
224
270
  /**
225
271
  * Identity helper that brands an object as a typed `Message`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amos.com/amos-js",
3
- "version": "0.9.17",
3
+ "version": "0.10.0",
4
4
  "main": "dist/index.js",
5
5
  "repository": {
6
6
  "type": "git",
package/src/index.ts CHANGED
@@ -52,6 +52,7 @@ export {
52
52
  export type {
53
53
  AmosApplePayButtonMountController,
54
54
  AmosApplePayButtonOptions,
55
+ AmosBankAccountPaymentMethodFormMountController,
55
56
  AmosBankAccountPaymentMethodFormOptions,
56
57
  AmosCreditCardPaymentMethodFormOptions,
57
58
  AmosGooglePayButtonMountController,
@@ -77,14 +78,28 @@ export {
77
78
  getCreditCardFormInitialHeight,
78
79
  getCreditCardFormSrc,
79
80
  } from "./payment-method-form";
81
+ export type {
82
+ OpenPlaidLinkInput,
83
+ PlaidCredentials,
84
+ PlaidLinkOnSuccessMetadata,
85
+ } from "./plaid";
86
+ export {
87
+ linkedBankLabelFromMetadata,
88
+ loadPlaidScript,
89
+ openPlaidLink,
90
+ plaidAccountIdFromMetadata,
91
+ requiresAchVerification,
92
+ } from "./plaid";
80
93
  export type {
81
94
  Appearance,
82
95
  AppearanceLabels,
83
96
  ApplePayButtonElementProps,
97
+ CardBrand,
84
98
  ConfirmationIncompleteReason,
85
99
  ConfirmationResult,
86
100
  GooglePayButtonElementProps,
87
101
  Message,
102
+ PaymentMethodFormCardBrandChangeEvent,
88
103
  PaymentMethodFormValidityChangeEvent,
89
104
  ThemeVariable,
90
105
  } from "./types";
package/src/messaging.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { components } from "@amos.com/node";
2
2
  import { decodeJwt } from "./jwt";
3
+ import { getBankPlaidSession } from "./plaid-session";
3
4
  import {
4
5
  type Appearance,
5
6
  type ApplePayButtonElementProps,
@@ -163,6 +164,11 @@ export function updateMerchantName({
163
164
  * `false` if the iframe does not respond within 5 seconds.
164
165
  */
165
166
  export function validateForm({ iframe }: { iframe: Iframe }): Promise<boolean> {
167
+ const session = getBankPlaidSession(iframe);
168
+ if (session?.requiresVerification) {
169
+ return Promise.resolve(Boolean(session.plaid));
170
+ }
171
+
166
172
  const requestId = crypto.randomUUID();
167
173
 
168
174
  return new Promise((resolve) => {
@@ -193,11 +199,71 @@ export function validateForm({ iframe }: { iframe: Iframe }): Promise<boolean> {
193
199
  });
194
200
  }
195
201
 
202
+ const PLAID_LINK_TOKEN_TIMEOUT_MS = 10_000;
203
+
204
+ /**
205
+ * Ask the bank iframe to mint a Plaid Link token (`POST /plaid_link_tokens`
206
+ * on embed). Resolves with `link_token`, or rejects on error / timeout.
207
+ */
208
+ export function requestPlaidLinkToken({
209
+ iframe,
210
+ }: {
211
+ iframe: Iframe;
212
+ }): Promise<string> {
213
+ const requestId = crypto.randomUUID();
214
+
215
+ return new Promise((resolve, reject) => {
216
+ if (!iframe?.contentWindow) {
217
+ reject(new Error("Bank form is not ready."));
218
+ return;
219
+ }
220
+
221
+ const contentWindow = iframe.contentWindow;
222
+ contentWindow.postMessage(
223
+ createMessage({ type: "CREATE_PLAID_LINK_TOKEN", requestId }),
224
+ getIframeTargetOrigin(iframe),
225
+ );
226
+
227
+ const timeoutId = setTimeout(() => {
228
+ window.removeEventListener("message", handleMessage);
229
+ reject(new Error("Timed out waiting for Plaid Link token."));
230
+ }, PLAID_LINK_TOKEN_TIMEOUT_MS);
231
+
232
+ function handleMessage(event: MessageEvent<Message>) {
233
+ if (event.source !== contentWindow) {
234
+ return;
235
+ }
236
+ if (
237
+ event.data.type !== "PLAID_LINK_TOKEN" ||
238
+ event.data.requestId !== requestId
239
+ ) {
240
+ return;
241
+ }
242
+ window.removeEventListener("message", handleMessage);
243
+ clearTimeout(timeoutId);
244
+ if (event.data.link_token) {
245
+ resolve(event.data.link_token);
246
+ return;
247
+ }
248
+ reject(
249
+ new Error(event.data.error ?? "Could not create Plaid Link token."),
250
+ );
251
+ }
252
+
253
+ window.addEventListener("message", handleMessage);
254
+ });
255
+ }
256
+
196
257
  /**
197
258
  * Clear all field values and API errors in the embedded credit-card or
198
259
  * bank-account iframe form.
199
260
  */
200
261
  export function resetForm({ iframe }: { iframe: Iframe }): void {
262
+ getBankPlaidSession(iframe)?.clearLinked?.();
263
+ resetIframeFields(iframe);
264
+ }
265
+
266
+ function resetIframeFields(iframe: Iframe): void {
201
267
  if (!iframe?.contentWindow) {
202
268
  return;
203
269
  }
@@ -208,6 +274,35 @@ export function resetForm({ iframe }: { iframe: Iframe }): void {
208
274
  );
209
275
  }
210
276
 
277
+ function postConfirmIntent({
278
+ iframe,
279
+ type,
280
+ token,
281
+ id,
282
+ }: {
283
+ iframe: HTMLIFrameElement;
284
+ type: "CONFIRM_PAYMENT_INTENT" | "CONFIRM_SETUP_INTENT";
285
+ token: string | undefined;
286
+ id: string | undefined;
287
+ }): void {
288
+ const session = getBankPlaidSession(iframe);
289
+ const plaid = session?.plaid;
290
+ // Plaid mode: do not confirm leftover routing/account numbers.
291
+ if (session?.requiresVerification && !plaid) {
292
+ resetIframeFields(iframe);
293
+ }
294
+
295
+ iframe.contentWindow?.postMessage(
296
+ createMessage({
297
+ type,
298
+ token,
299
+ id,
300
+ ...(plaid ? { plaid } : {}),
301
+ }),
302
+ getIframeTargetOrigin(iframe),
303
+ );
304
+ }
305
+
211
306
  /**
212
307
  * Confirm a payment intent in the embedded iframe flow.
213
308
  *
@@ -227,14 +322,12 @@ export function confirmPaymentIntent({
227
322
 
228
323
  const { payment_intent_id: id }: components["schemas"]["EmbedTokenJwt"] =
229
324
  decodeJwt(token).payload;
230
- iframe.contentWindow.postMessage(
231
- createMessage({
232
- type: "CONFIRM_PAYMENT_INTENT",
233
- token,
234
- id: id ?? undefined,
235
- }),
236
- getIframeTargetOrigin(iframe),
237
- );
325
+ postConfirmIntent({
326
+ iframe,
327
+ type: "CONFIRM_PAYMENT_INTENT",
328
+ token,
329
+ id: id ?? undefined,
330
+ });
238
331
  }
239
332
 
240
333
  /**
@@ -256,14 +349,12 @@ export function confirmSetupIntent({
256
349
 
257
350
  const { setup_intent_id: id }: components["schemas"]["EmbedTokenJwt"] =
258
351
  decodeJwt(token).payload;
259
- iframe.contentWindow.postMessage(
260
- createMessage({
261
- type: "CONFIRM_SETUP_INTENT",
262
- token,
263
- id: id ?? undefined,
264
- }),
265
- getIframeTargetOrigin(iframe),
266
- );
352
+ postConfirmIntent({
353
+ iframe,
354
+ type: "CONFIRM_SETUP_INTENT",
355
+ token,
356
+ id: id ?? undefined,
357
+ });
267
358
  }
268
359
 
269
360
  /**
package/src/mount.ts CHANGED
@@ -31,6 +31,7 @@ import {
31
31
  type PaymentMethodFormController,
32
32
  type PaymentMethodFormListenerOptions,
33
33
  } from "./payment-method-form";
34
+ import { attachPlaidBankUi } from "./plaid-bank-ui";
34
35
 
35
36
  type Container = HTMLElement | string;
36
37
 
@@ -425,6 +426,18 @@ export type AmosPaymentMethodFormMountController =
425
426
  iframe: HTMLIFrameElement;
426
427
  };
427
428
 
429
+ /**
430
+ * Controller returned by {@link mountAmosBankAccountPaymentMethodForm}.
431
+ * `update()` also accepts `amount` (major-currency decimal string) when
432
+ * the charge changes.
433
+ */
434
+ export type AmosBankAccountPaymentMethodFormMountController = Omit<
435
+ AmosPaymentMethodFormMountController,
436
+ "update"
437
+ > & {
438
+ update: (patch: Partial<AmosBankAccountPaymentMethodFormOptions>) => void;
439
+ };
440
+
428
441
  /**
429
442
  * Mount the secure credit-card payment method form into a container
430
443
  * element. Returns a controller exposing the underlying iframe, an
@@ -494,6 +507,14 @@ export type AmosBankAccountPaymentMethodFormOptions =
494
507
  * @default "country"
495
508
  */
496
509
  billingAddressRequirement?: BillingAddressRequirement;
510
+ /**
511
+ * Charge amount as a major-currency decimal string (e.g. `"50.00"`
512
+ * for $50.00), the same format as Google Pay / Apple Pay. Compared
513
+ * to the merchant ACH threshold fetched by the iframe. Omit for
514
+ * setup intents or when the charge is unknown — if a threshold is
515
+ * set, Plaid is required.
516
+ */
517
+ amount?: string;
497
518
  };
498
519
 
499
520
  /**
@@ -501,8 +522,12 @@ export type AmosBankAccountPaymentMethodFormOptions =
501
522
  * element. Returns a controller exposing the underlying iframe, an
502
523
  * `update()` method, and a `destroy()` method.
503
524
  *
504
- * A field-shaped skeleton is shown immediately and replaced by the
505
- * iframe once appearance is applied.
525
+ * When the iframe reports an ACH threshold and `amount` meets it (or
526
+ * `amount` is omitted), a Connect bank button is rendered in the parent
527
+ * document and Plaid Link is opened on click. The button uses the same
528
+ * `appearance.themeVariables` as the iframe (and inherits host-page
529
+ * tokens when those variables are unset). Otherwise a field-shaped
530
+ * skeleton is shown and replaced by the iframe once appearance is applied.
506
531
  *
507
532
  * Use the returned `controller.iframe` when calling
508
533
  * {@link validateForm}, {@link confirmPaymentIntent}, or
@@ -511,11 +536,12 @@ export type AmosBankAccountPaymentMethodFormOptions =
511
536
  export function mountAmosBankAccountPaymentMethodForm(
512
537
  container: Container,
513
538
  options: AmosBankAccountPaymentMethodFormOptions,
514
- ): AmosPaymentMethodFormMountController {
539
+ ): AmosBankAccountPaymentMethodFormMountController {
515
540
  const host = resolveContainer(container);
516
541
  const {
517
542
  renderToken,
518
543
  billingAddressRequirement = "country",
544
+ amount,
519
545
  ...listenerOptions
520
546
  } = options;
521
547
 
@@ -526,7 +552,7 @@ export function mountAmosBankAccountPaymentMethodForm(
526
552
  height: getBankAccountFormInitialHeight(billingAddressRequirement),
527
553
  });
528
554
 
529
- return mountPaymentMethodFormWithSkeleton({
555
+ const iframeMount = mountPaymentMethodFormWithSkeleton({
530
556
  host,
531
557
  iframe,
532
558
  listenerOptions,
@@ -536,6 +562,28 @@ export function mountAmosBankAccountPaymentMethodForm(
536
562
  billingAddressRequirement,
537
563
  },
538
564
  });
565
+
566
+ const plaidUi = attachPlaidBankUi({
567
+ host,
568
+ iframe,
569
+ options: {
570
+ amount,
571
+ appearance: listenerOptions.appearance,
572
+ onValidityChange: listenerOptions.onValidityChange,
573
+ },
574
+ });
575
+
576
+ return {
577
+ iframe,
578
+ update(patch) {
579
+ iframeMount.update(patch as Partial<PaymentMethodFormListenerOptions>);
580
+ plaidUi.update(patch);
581
+ },
582
+ destroy() {
583
+ plaidUi.destroy();
584
+ iframeMount.destroy();
585
+ },
586
+ };
539
587
  }
540
588
 
541
589
  /**
@@ -3,10 +3,12 @@ import {
3
3
  sendParentReadyMessage,
4
4
  updateAppearance as sendUpdateAppearance,
5
5
  } from "./messaging";
6
+ import { getBankPlaidSession } from "./plaid-session";
6
7
  import type {
7
8
  Appearance,
8
9
  ConfirmationResult,
9
10
  Message,
11
+ PaymentMethodFormCardBrandChangeEvent,
10
12
  PaymentMethodFormValidityChangeEvent,
11
13
  } from "./types";
12
14
 
@@ -137,6 +139,13 @@ export type PaymentMethodFormListenerOptions = {
137
139
  * PCI data. Use this to enable or disable a host checkout button.
138
140
  */
139
141
  onValidityChange?: (event: PaymentMethodFormValidityChangeEvent) => void;
142
+ /**
143
+ * Called when the detected card brand changes. `brand` is the matched
144
+ * network, or `null` when the field is empty or the digits do not
145
+ * match a known brand. Does not include PCI data. Credit-card form
146
+ * only — never fired for bank account.
147
+ */
148
+ onCardBrandChanged?: (event: PaymentMethodFormCardBrandChangeEvent) => void;
140
149
  /**
141
150
  * Called when the interactive confirmation flow finishes (success or
142
151
  * terminal failure). Not settlement proof — verify via webhooks.
@@ -203,9 +212,16 @@ export function attachPaymentMethodFormListeners(
203
212
  break;
204
213
 
205
214
  case "FORM_VALIDITY_CHANGE":
215
+ if (getBankPlaidSession(iframe)?.requiresVerification) {
216
+ break;
217
+ }
206
218
  current.onValidityChange?.({ isValid: event.data.isValid });
207
219
  break;
208
220
 
221
+ case "CARD_BRAND_CHANGE":
222
+ current.onCardBrandChanged?.({ brand: event.data.brand });
223
+ break;
224
+
209
225
  case "CONFIRMATION_RESULT":
210
226
  current.onResult(event.data.result);
211
227
  break;