Skip to content

feat: added support for separate card fields - #1742

Open
ArushKapoorJuspay wants to merge 16 commits into
mainfrom
feat/separate-card-fields
Open

feat: added support for separate card fields#1742
ArushKapoorJuspay wants to merge 16 commits into
mainfrom
feat/separate-card-fields

Conversation

@ArushKapoorJuspay

@ArushKapoorJuspay ArushKapoorJuspay commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Type of Change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring
  • Dependency updates
  • Documentation
  • CI/CD

Description

Adds separate card fields to the Web SDK: merchants can mount cardNumber, cardExpiry and cardCvc as three independently positioned elements instead of one bundled card form, and lay them out freely in their own checkout.

Today the SDK only offers a single combined card element, so any merchant wanting card number and expiry on different rows — or a saved-card CVC box on its own — has to either accept our layout or leave Hyperswitch's iframes entirely and take on PCI scope themselves. This PR closes that gap without moving card data into the merchant page.

Two surfaces are supported, sharing one implementation:

Surface Entry point Settles with API called (from the coordinator)
Payments hyper.widgets({ clientSecret }).cardForm() confirmPayment() PaymentHelpers.usePaymentIntent
Vault hyper.paymentMethodsSession({ sdkAuthorization }).cardForm() tokenize() PaymentHelpersV2.savePaymentMethod / updatePaymentMethod

Types.cardForm (payments) and Types.vaultCardForm (vault) are distinct types: the vault path stores a payment method and resolves the vault response, so it exposes tokenize() rather than confirmPayment().


Merchant integration

Payments — collect a new card

Get a card form off the widgets instance, create the three fields, mount each one into your own container, and confirm. Options passed to create() are per field; anything you don't pass falls back to the group's appearance and locale.

const hyper = window.Hyper(publishableKey);
const cardForm = hyper.widgets({ clientSecret, appearance, locale: "en" }).cardForm();

const number = cardForm.create("cardNumber", { placeholder: "Card number" });
const expiry = cardForm.create("cardExpiry", { placeholder: "MM / YY" });
const cvc    = cardForm.create("cardCvc",    { placeholder: "CVC" });

number.mount("#card-number");
expiry.mount("#card-expiry");
cvc.mount("#card-cvc");

document.querySelector("#pay").addEventListener("click", async () => {
  const result = await cardForm.confirmPayment();

  // Success: the confirm-intent response body, verbatim — same object
  // `hyper.confirmPayment()` resolves with (result.status, result.payment_id, ...).
  // Failure: result.error = { type, message, code }
  // confirmPayment() never rejects, so always branch on `result.error`.
  if (result.error) showError(result.error.message);
  else onPaid(result);
});

Only the three field types above are accepted; any other string logs invalid_field_type and hands back an inert handle. Each fieldHandle supports mount · unmount · destroy · update · focus · blur · clear · on. Focus auto-advances number → expiry → CVC as each field completes, decided inside the field's own iframe.

cardForm.update(options) forwards new options to every mounted field. It deliberately refuses clientSecret and confirmParams (they are immutable after mount) with a console warning — create a new group to switch intents. cardForm.deinit() destroys the fields, tears down the coordinator, and settles any in-flight confirmPayment() with a group_deinitialized error.

Vault — tokenize into a payment method session

Same shape, different entry point and a tokenize() instead of confirmPayment(). The SDK resolves the vault configuration itself from GET /v1/payment-method-sessions/{id}, reusing the sdkAuthorization you already passed, so there is nothing extra to wire up.

const session = hyper.paymentMethodsSession({ sdkAuthorization, appearance, locale: "en" });
const cardForm = session.cardForm();

cardForm.create("cardNumber", { placeholder: "Card number" }).mount("#card-number");
cardForm.create("cardExpiry", {}).mount("#card-expiry");
cardForm.create("cardCvc",    {}).mount("#card-cvc");

const result = await cardForm.tokenize();

// Success: the POST /v1/payment-method-sessions/{id}/confirm response body, verbatim.
// Failure: result.error = { type, message, code }
// tokenize() never rejects either.
if (result.error) showError(result.error.message);

A session is single-use: a successful tokenize() marks it consumed, and further calls resolve { error: { code: "session_consumed", ... } }. An expired session (expires_at from the retrieve call) resolves session_expired, and a second concurrent call resolves tokenization_in_progress. session.update() is a no-op that warns — session options are fixed at creation.

If the session's external_vault_details name VGS, the same cardForm() code mounts VGS Collect.js fields into your DOM instead of Hyperswitch iframes. Nothing in the snippet above changes.

Saved-card CVC recollect

Mount only cardCvc and pass the stored card's token and brand. The brand sizes and validates the CVC box (3 vs 4 digits); the token identifies the saved method at confirm time.

const cvc = cardForm.create("cardCvc", { savedCard: { token: paymentToken, brand: "Visa" } });
cvc.mount("#saved-card-cvc");

const result = await cardForm.confirmPayment(); // uses the savedCardCvc flow automatically

You can also supply it later with cvc.update({ savedCard: { token, brand } }). On the payments surface token is required — confirming without one resolves a validation_error naming the fix. On the vault surface, mounting only cardCvc runs the CVC-update flow and just the brand is read.

Field events

Subscribe per field with field.on(event, cb), or at the group with cardForm.on(event, cb). One callback per event name per target — a second on() for the same name replaces the first.

number.on("ready",  (e) => {});                       // e: { elementType, iframeId }
number.on("focus",  (e) => {});                       // e: { elementType, iframeId }
number.on("blur",   (e) => {});                       // e: { elementType, iframeId }
number.on("change", (e) => {
  // e: { elementType: "cardNumber", empty, complete, valid, brand?, error? }
  setDisabled(!e.complete);
  setInlineError(e.error ?? "");
});

// Group level.
cardForm.on("ready",   () => {});   // payments only: all three fields now complete
cardForm.on("unready", () => {});   // payments only: they stopped being complete
cardForm.on("error",   (e) => {});  // confirm/tokenize-time failure

ready · focus · blur · change fire on every hosted field, and on VGS fields too. field.on("error", ...) carries { elementType, iframeId, message } and is vault-only — on the payments surface, confirm-time validation failures arrive on the group's error ({ elementType: "paymentsCoordinator", iframeId, code, message }) rather than per field. The payments group additionally emits confirmDispatched once the confirm has left the coordinator. The vault group's error carries the same { error: { type, message, code } } envelope that tokenize() resolves with.

Per-field status — cardFieldStatusInfo

A registered subscription event that reports a single field's form status. It is opt-in: list it in subscriptionEvents in that field's create() options, then subscribe with the same name.

const number = cardForm.create("cardNumber", {
  placeholder: "Card number",
  subscriptionEvents: ["cardFieldStatusInfo"],
});

number.on("cardFieldStatusInfo", (e) => {
  // e: { elementType, iframeId, eventName: "cardFieldStatusInfo",
  //      payload: { status, message?, cardBrand? } }
  // status: "complete" | "incomplete" | "invalid" | "focused" | "blurred"
  // message is present only on "invalid"; cardBrand only once a brand is detected.
  render(e.payload.status);
});

cardForm.on("cardFieldStatusInfo", (e) => {}); // same payload, all opted-in fields

A field that does not list it in subscriptionEvents emits nothing, whatever you subscribe to. This event is hosted-field only — the VGS path does not emit it.

Styling

Every field inherits the group appearance and locale passed to widgets() / paymentMethodsSession(). A field can also carry its own appearance in create(), and that override is wholesale, not a merge: if the field's appearance object has any keys at all, the group's appearance is dropped entirely for that field. A field supplying only variables therefore loses the group's rules as well, so a per-field override has to restate everything it still wants.

const appearance = {
  variables: { colorPrimary: "#0570de", cardFieldHeight: "44px" },
  rules: { ".Input": { borderRadius: "8px" } },
};

const cardForm = hyper.widgets({ clientSecret, appearance, locale: "en" }).cardForm();

// No `appearance` key -> inherits the group's, in full.
cardForm.create("cardExpiry", { placeholder: "MM / YY" });

// Has an `appearance` key -> replaces the group's, in full.
// Dropping `rules` here would leave this one field unstyled by it.
cardForm.create("cardNumber", {
  placeholder: "Card number",
  cardBrandIcon: "animated",
  appearance: {
    variables: { colorPrimary: "#111827", cardFieldHeight: "56px" },
    rules: { ".Input": { borderRadius: "8px" } },
  },
});

appearance.variables.cardFieldHeight (default 48px) sets the height of the element the field occupies — the field's iframe on the hosted path, the VGS-owned container on the VGS path — and is the one appearance variable that only means anything for separate card fields.

Per-field options, hosted fields. These are read out of each field's create() options and apply to both the payments and vault hosted paths.

Option Fields Values
placeholder all three any string; "" renders no placeholder
cardBrandIcon cardNumber standard · hidden · animated · hideGeneric
cvcIcon cardCvc hidden · default
appearance all three an appearance object; replaces the group's for that field
savedCard cardCvc { token, brand } — see saved-card CVC recollect
subscriptionEvents all three ["cardFieldStatusInfo"] — see per-field status

Both enums are validated by value: an unrecognised string warns (Unknown Value: 'foo' value in options.cardBrandIcon, Expected …) and is then ignored, leaving the icon style as it was. Every key is presence-gated, on create() and on field.update() alike — a key you leave out is not reset to its default, it is simply left alone, so update() behaves as a patch rather than a replace.

Per-field options, VGS only. When the session's external_vault_details name VGS, these extra keys are merged over the SDK's per-field defaults and handed to form.field(...) verbatim. They are not read on the hosted-iframe path, which parses only the table above.

Option Type Notes
placeholder string also honoured on hosted fields; SDK defaults 1234 1234 1234 1234 · MM / YY · 123
successColor / errorColor string forwarded to VGS as-is
ariaLabel string forwarded to VGS as-is
autoComplete string forwarded to VGS as-is
inputMode string forwarded to VGS as-is
defaultValue string forwarded to VGS as-is
showCardIcon bool SDK default true on cardNumber and cardCvc, false on cardExpiry and on the saved-card cardCvc
disabled / readOnly / hideValue bool forwarded to VGS as-is
yearLength int cardExpiry only; SDK default 2
css object merged key-wise over the SDK's per-field default, rather than replacing it

field.update() on the VGS path honours a narrower set — placeholder, ariaLabel, autoComplete, css, hideValue, disabled, readOnly, showCardIcon — and every other key is dropped before the call reaches VGS; successColor, inputMode, yearLength and friends are therefore create-time only.

One trap worth flagging: yearLength: 4 makes VGS require a four-digit year, so a shopper typing the perfectly valid-looking 12 / 28 is rejected. Pair it with a matching placeholder (MM / YYYY) or leave it at the default 2.

Errors

Both confirmPayment() and tokenize() resolve rather than reject, matching hyper.confirmPayment. A failure is any resolved object carrying error:

// { error: { type: "validation_error" | "api_error" | "card_error",
//            message: "…",
//            code: "…" } }
if (result.error) showError(result.error.message);

The two surfaces raise different codes — they share the envelope, not the vocabulary.

confirmPayment() (payments)

code When
validation_error no card fields mounted, or saved-card CVC without a token
confirm_in_progress a second confirmPayment() while one is in flight
group_deinitialized deinit() was called mid-confirm

tokenize() (vault)

code When
validation_error a mounted field is empty or invalid at submit time
incomplete_field_set neither a full card nor a lone cardCvc is mounted
session_expired · session_consumed the session's expires_at passed, or it was already tokenized
tokenization_in_progress a second tokenize() while one is in flight
tokenization_failed the vault call itself failed
vgs_form_not_ready VGS path only — submit before the form initialised

On the VGS path, mount- and field-level failures do not resolve the promise; they arrive on cardForm.on("error") as vgs_mount_failed, vgs_field_event_binding_failed, vgs_field_update_failed or vgs_field_unmount_failed, since they happen outside a tokenize() call.

A payments failure relayed from the confirm call itself passes through the same envelope hyper.confirmPayment resolves (submitSuccessful: false plus error: { type, message }) and carries no code, so branch on result.error, not on result.error.code.

There are no timeouts on either promise — again matching hyper.confirmPayment, which has none. Commands issued before the coordinator reports ready are queued and flushed, so a confirm racing the coordinator's boot is never dropped.


Architecture

Each field renders in its own cross-origin iframe. The centrepiece is the coordinator — one hidden 0×0 iframe per card form, and the single place every card-data API call is made from.

Both surfaces reach it the same way: an entry point returns an object exposing cardForm(), and that card form creates and mounts the individual fields.

  PAYMENTS                                VAULT / PAYMENT METHODS SESSION

  hyper.widgets({ clientSecret })         hyper.paymentMethodsSession({ sdkAuthorization })
            │                                       │
            ▼                                       ▼
      .cardForm()                             .cardForm()
      Types.cardForm                          Types.vaultCardForm
            │                                       │
            └───────────────┬───────────────────────┘
                            ▼
              cardForm.create("cardNumber" | "cardExpiry" | "cardCvc")
                            │
                            ▼
                    fieldHandle.mount("#selector")
                            │
                            ▼
              cardForm.confirmPayment()   |   cardForm.tokenize()

Flow 1 — Payments (hyper.widgets(...).cardForm())

┌─ MERCHANT PAGE ─────────────────────────────────────────────────────────┐
│                                                                         │
│   ┌────────────┐      ┌────────────┐      ┌────────────┐                │
│   │ cardNumber │      │ cardExpiry │      │  cardCvc   │                │
│   │  <iframe>  │      │  <iframe>  │      │  <iframe>  │                │
│   └──┬──────┬──┘      └──┬──────┬──┘      └──┬──────┬──┘                │
│      │      │            │      │            │      │                   │
│   masked   raw        masked   raw        masked   raw                  │
│      │      │            │      │            │      │                   │
│      ▼      │            ▼      │            ▼      │                   │
│   ┌─────────┼───────────────────┼───────────────────┼──────┐            │
│   │         │   PaymentsGroup   │                   │      │            │
│   │         │                   │                   │      │            │
│   │  • aggregates masked state, emits merchant events      │            │
│   │  • creates the MessageChannel, wires both ends         │            │
│   │  • owns the confirm mutex                              │            │
│   │  • API: fetchClientList only  (no card data)           │            │
│   └─────────┼───────────────────┼───────────────────┼──────┘            │
│             │                   │                   │                   │
│      initiateConfirm     MessageChannel ports (raw card data)           │
│             │                   │                   │                   │
│             ▼                   ▼                   ▼                   │
│   ╔═════════════════════════════════════════════════════════╗           │
│   ║  cardFormCoordinator   <iframe hidden 0×0>              ║           │
│   ║  surfaceFamily=payments                                 ║           │
│   ║                                                         ║           │
│   ║  assembles the card payload · issues the API call       ║           │
│   ╚════════════════════════════╤════════════════════════════╝           │
└────────────────────────────────┼────────────────────────────────────────┘
                                 ▼
                    PaymentHelpers.usePaymentIntent
                       POST  confirm payment intent

Flow 2 — Vault (hyper.paymentMethodsSession(...).cardForm())

┌─ MERCHANT PAGE ─────────────────────────────────────────────────────────┐
│                                                                         │
│   ┌────────────┐      ┌────────────┐      ┌────────────┐                │
│   │ cardNumber │      │ cardExpiry │      │  cardCvc   │                │
│   │  <iframe>  │      │  <iframe>  │      │  <iframe>  │                │
│   └──┬──────┬──┘      └──┬──────┬──┘      └──┬──────┬──┘                │
│      │      │            │      │            │      │                   │
│   masked   raw        masked   raw        masked   raw                  │
│      │      │            │      │            │      │                   │
│      ▼      │            ▼      │            ▼      │                   │
│   ┌─────────┼───────────────────┼───────────────────┼──────┐            │
│   │         │ PaymentMethodsSession                 │      │            │
│   │         │                   │                   │      │            │
│   │  • same responsibilities as PaymentsGroup              │            │
│   │  • plus session lifecycle: expiry, consumed state      │            │
│   │  • API: GET /v1/payment-method-sessions/{id}           │            │
│   │         (config only — no card data)                   │            │
│   └─────────┼───────────────────┼───────────────────┼──────┘            │
│             │                   │                   │                   │
│      initiateConfirm     MessageChannel ports (raw card data)           │
│             │                   │                   │                   │
│             ▼                   ▼                   ▼                   │
│   ╔═════════════════════════════════════════════════════════╗           │
│   ║  cardFormCoordinator   <iframe hidden 0×0>              ║           │
│   ║  surfaceFamily=vault                                    ║           │
│   ║                                                         ║           │
│   ║  assembles the card payload · issues the API call       ║           │
│   ╚════════════════════════════╤════════════════════════════╝           │
└────────────────────────────────┼────────────────────────────────────────┘
                                 ▼
                       PaymentHelpersV2
              savePaymentMethod  /  updatePaymentMethod

When the session's provider is VGS, this flow swaps the field iframes and the coordinator for VGS Collect.js fields injected into the merchant DOM; the cardForm() API is unchanged.

Why a coordinator

Three iframes each hold one third of a card. Something has to assemble them before an API call can be made — and the obvious candidate, the merchant page, is exactly where card data must not go.

The coordinator is that assembly point, moved out of the merchant page into an SDK-controlled iframe. Every field's raw value reaches it directly over a MessageChannel port, bypassing the merchant window entirely.

The group's role deserves precision, because it looks like it handles more than it does. The group creates each MessageChannel and wires it up — port2 travels to the field with its mount config, port1 is forwarded into the coordinator — but it never reads a port. It is the switchboard operator who connects the call and cannot hear it. Everything the group itself consumes is masked: validity flags, brand, masked card info, and a focusReady flag used to auto-advance focus.

Call Flow Origin
PaymentHelpers.usePaymentIntent payments, savedCardCvc coordinator
PaymentHelpersV2.savePaymentMethod save coordinator
PaymentHelpersV2.updatePaymentMethod update coordinator
PaymentHelpers.fetchClientList payments pre-warm group (no card data)
PaymentHelpersV2.retrievePaymentMethodSession vault session config group (no card data)

The field iframes make no network calls at all — they are pure emitters.

What the coordinator returns to the group is the API response itself: the payments arm posts the SDK's standard {submitSuccessful, data, url} message, the vault arm posts {confirmResult, confirmId} carrying the vault response — never the card values it assembled. The merchant page sees a payment outcome without ever having seen a card number.

Each coordinator mount also carries its own 3DS fullscreen slot, which is why two card forms on one page can each run a challenge without clobbering the other.

Two planes

The core of the design. Field state is split across two transports so raw card data never touches the merchant window.

                  ┌────────────────────────┐
                  │      field iframe      │
                  │      (cardNumber)      │
                  └───────────┬────────────┘
                              │
              ┌───────────────┴───────────────┐
              │                               │
              ▼                               ▼
        WINDOW PLANE                     PORT PLANE
        postMessage                    MessageChannel

  ┌────────────────────────┐      ┌────────────────────────┐
  │ cardBrand              │      │ cardBrand              │
  │ fieldStatus            │      │ fieldStatus            │
  │ cardInfo    (masked)   │      │ cardInfo    (masked)   │
  │ focusReady             │      │ focusReady             │
  │                        │      │ rawCardNumber      ⚠   │
  │      NO RAW DATA       │      │ rawCardExpiry      ⚠   │
  │                        │      │ rawCvc             ⚠   │
  └───────────┬────────────┘      └───────────┬────────────┘
              │                               │
              ▼                               ▼
       merchant page                  coordinator iframe
      (group + events)                (confirm payload)

Both halves are produced from one encoder — CardFormPortProtocol.encodeFieldStateUpdate — in a single effect, so the two planes can never disagree about a field's state. Port frames are versioned: {cardFormPortV, kind, payload}.

A MessageChannel is created per field per epoch. port2 rides with the field's mount-config message via postMessage transfer; port1 is retained and forwarded into the coordinator once it reports mounted. Both documents absorb ev.ports[0] into SadPortRegistry, keyed "<groupId>:<fieldName>". Re-installing under the same key with a new epoch closes the superseded port, so a remount can never leave a live listener on a dead channel.

Confirm

  merchant            group        field iframes     coordinator          API
     │                  │               │                 │                │
     │                  │      keystrokes                 │                │
     │                  │               │                 │                │
     │                  │               │──── raw ───────►│  cached        │
     │                  │               │     (port)      │  per field     │
     │                  │◄── masked ────│                 │                │
     │                  │               │                 │                │
     │ confirmPayment() │               │                 │                │
     │ tokenize()       │               │                 │                │
     │─────────────────►│               │                 │                │
     │                  │               │                 │                │
     │                  │── initiateConfirm ─────────────►│  assembles     │
     │                  │   (content-free)                │  payload       │
     │                  │               │                 │                │
     │                  │               │                 │───────────────►│
     │                  │               │                 │                │
     │                  │               │                 │◄───────────────│
     │                  │               │                 │                │
     │                  │◄── submitSuccessful | confirmResult ──────────────│
     │                  │               │                 │                │
     │◄─ response ──────│               │                 │                │
     │                  │               │                 │                │

The confirm command itself carries no card data — it is just {cardFormCoordinatorCommand: "initiateConfirm", flow, confirmId} (plus paymentToken for saved-card CVC, savedCardBrand/locale on the vault side). The coordinator already has everything it needs from the port plane. The merchant page never handles a card value at any point in this sequence.

flow is one of payments · savedCardCvc · save · update, and the coordinator refuses a command whose flow does not match its own surfaceFamily URL parameter, so a vault command can never be serviced by a payments coordinator or vice versa.

VGS direct injection

When the session's external_vault_details name VGS, VGSVaultBroker.res loads VGS Collect.js and mounts VGS's own fields directly into the merchant DOM, bypassing Hyperswitch field iframes and the coordinator entirely; tokenize() then settles through form.submit. The merchant-facing API is identical; only the transport differs.

Flows supported

Flow Description
payments new card → payment intent confirm
savedCardCvc saved card + CVC recollect → confirm with payment_token
save vault tokenization of a new card
update vault CVC re-collection on a saved card

Security notes

  • Raw PAN / expiry / CVC ride the MessageChannel port plane only; the window plane carries masked state exclusively.
  • Port ingestion is gated on ev.source and origin, and only absorbs a port when the same message carries a handshake key (paymentElementCreate or cardFieldPort) and a non-empty portKey.
  • Auto-advance between fields is driven by a focusReady flag computed inside each field's own iframe, so no keystroke timing leaves the iframe.

Breaking changes

All three affect anyone integrated against an earlier revision of this branch; none of them touches the existing bundled card element.

  1. cardForm.confirm() is now cardForm.confirmPayment(). The payments card form's settle method was renamed for parity with the SDK's top-level hyper.confirmPayment(), whose contract it already mirrors — resolves the confirm-intent response verbatim on success, { error: { type, message, code } } on failure, never rejects. Nothing but the name changed. The vault surface is unaffected and keeps tokenize().
  2. formStatusChange is now cardFieldStatusInfo. The ad-hoc per-field status event was replaced by a registered subscription event. Rename the listener; the payload is now the {elementType, iframeId, eventName, payload: {status, message?, cardBrand?}} envelope every other subscription event uses.
  3. cardFieldStatusInfo is opt-in. It only fires for fields whose create() options list it in subscriptionEvents. A listener alone is no longer enough.

onFieldEvent — a group-level fan-out that never reached Types.cardForm — was removed. Use field.on(...) and cardForm.on(...).

Files added

CardFormCoordinator · CardFormPortProtocol · SadPortRegistry · MessageChannelBinding · CardCollectorBridge · CommonCardFieldHooks · SecureCardNumberField · SecureCardExpiryField · SecureCardCvcField · PaymentSurfaceFamily · CardFormShared · CardFormGroupShared · CoordinatorMount · PaymentsGroup · PaymentMethodsSession · VGSVaultBroker

How did you test it?

Checklist

  • I ran npm run re:build
  • I reviewed submitted code
  • I added unit tests for my changes where possible

@semanticdiff-com

semanticdiff-com Bot commented Sep 1, 2026

Copy link
Copy Markdown

Review changes with  SemanticDiff

Changed Files
File Status
  src/App.res Unsupported file format
  src/BrutalTheme.res Unsupported file format
  src/BubblegumTheme.res Unsupported file format
  src/CardSchemeComponent.res Unsupported file format
  src/CardTheme.res Unsupported file format
  src/CharcoalTheme.res Unsupported file format
  src/Components/PaymentInputField.res Unsupported file format
  src/DefaultTheme.res Unsupported file format
  src/Hooks/SubscriptionEventHooks.res Unsupported file format
  src/LoaderController.res Unsupported file format
  src/MidnightTheme.res Unsupported file format
  src/NoTheme.res Unsupported file format
  src/Payments/CardCollectorBridge.res Unsupported file format
  src/Payments/CardFormCoordinator.res Unsupported file format
  src/Payments/CardFormPortProtocol.res Unsupported file format
  src/Payments/CommonCardFieldHooks.res Unsupported file format
  src/Payments/ParentCardComponent.res Unsupported file format
  src/Payments/PaymentMethodsSDK.res Unsupported file format
  src/Payments/SadPortRegistry.res Unsupported file format
  src/Payments/SecureCardCvcField.res Unsupported file format
  src/Payments/SecureCardExpiryField.res Unsupported file format
  src/Payments/SecureCardNumberField.res Unsupported file format
  src/Payments/VGSVault.res Unsupported file format
  src/Payments/VaultHelpers.res Unsupported file format
  src/SoftTheme.res Unsupported file format
  src/Types/CardThemeType.res Unsupported file format
  src/Types/PaymentType.res Unsupported file format
  src/Types/SubscriptionEventTypes.res Unsupported file format
  src/Types/VGSTypes.res Unsupported file format
  src/Utilities/CardFormShared.res Unsupported file format
  src/Utilities/JotaiAtoms.res Unsupported file format
  src/Utilities/MessageChannelBinding.res Unsupported file format
  src/Utilities/PaymentHelpersV2.res Unsupported file format
  src/Utilities/PaymentSurfaceFamily.res Unsupported file format
  src/Utilities/Utils.res Unsupported file format
  src/Utilities/VGSConstants.res Unsupported file format
  src/hyper-loader/CardFormGroupShared.res Unsupported file format
  src/hyper-loader/CoordinatorMount.res Unsupported file format
  src/hyper-loader/Elements.res Unsupported file format
  src/hyper-loader/Hyper.res Unsupported file format
  src/hyper-loader/LoaderPaymentElement.res Unsupported file format
  src/hyper-loader/PaymentMethodsManagementElements.res Unsupported file format
  src/hyper-loader/PaymentMethodsSession.res Unsupported file format
  src/hyper-loader/PaymentsGroup.res Unsupported file format
  src/hyper-loader/Types.res Unsupported file format
  src/hyper-loader/VGSVaultBroker.res Unsupported file format
  webpack.common.js  0% smaller

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

🚫 Missing Linked Issue

Hi 👋 This pull request does not appear to be linked to any open issue yet.

Linking your PR to an issue helps keep the project tidy and ensures the issue is closed automatically.

✔️ How to fix this

  • Add a keyword like Fixes #123 or Closes #456 to your PR description or a commit message.
  • Or link it manually using the "Linked issues" panel in the PR sidebar.

Tip: You can link multiple issues.
🚫 Note: If only one issue is linked, it must be open for this check to pass.

Once linked, this check will pass automatically on your next push or when you re-run the workflow.

Thanks for helping maintainers! 🙌

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

🚫 Missing Linked Issue

Hi 👋 This pull request does not appear to be linked to any open issue yet.

Linking your PR to an issue helps keep the project tidy and ensures the issue is closed automatically.

✔️ How to fix this

  • Add a keyword like Fixes #123 or Closes #456 to your PR description or a commit message.
  • Or link it manually using the "Linked issues" panel in the PR sidebar.

Tip: You can link multiple issues.
🚫 Note: If only one issue is linked, it must be open for this check to pass.

Once linked, this check will pass automatically on your next push or when you re-run the workflow.

Thanks for helping maintainers! 🙌

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

🚫 Missing Linked Issue

Hi 👋 This pull request does not appear to be linked to any open issue yet.

Linking your PR to an issue helps keep the project tidy and ensures the issue is closed automatically.

✔️ How to fix this

  • Add a keyword like Fixes #123 or Closes #456 to your PR description or a commit message.
  • Or link it manually using the "Linked issues" panel in the PR sidebar.

Tip: You can link multiple issues.
🚫 Note: If only one issue is linked, it must be open for this check to pass.

Once linked, this check will pass automatically on your next push or when you re-run the workflow.

Thanks for helping maintainers! 🙌

Comment thread src/Payments/VGSVault.res Outdated
Comment on lines +14 to +16
// `vgsScriptIntegrity` is imported from VGSConstants (see src/Utilities/VGSConstants.res)
// — co-located with `vgsScriptURL` so the two stay in lockstep when the pinned
// VGS version is bumped.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, there are extra comments in many files. Please remove those as well.

Comment thread src/LoaderController.res Outdated
~latency=renderLatency,
~value="",
)
applyShowCardIconOption(dict->getDictFromObj("options"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of creating a new function, applyShowCardIconOption, can we use only updateOptions since it can handle the combined logic?

Comment thread src/Utilities/PaymentSurfaceFamily.res Outdated
// `OtherFamily` is the loud-fail path (raises `InvalidSurfaceFamilyParams` upstream).
type surfaceFamily =
| VaultFamily // `componentName=paymentMethodsSDK&surfaceFamily=vault`
| PaymentsFamilyV2 // `componentName=paymentMethodsSDK&surfaceFamily=payments`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename it to something proper instead of V2

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🚫 Missing Linked Issue

Hi 👋 This pull request does not appear to be linked to any open issue yet.

Linking your PR to an issue helps keep the project tidy and ensures the issue is closed automatically.

✔️ How to fix this

  • Add a keyword like Fixes #123 or Closes #456 to your PR description or a commit message.
  • Or link it manually using the "Linked issues" panel in the PR sidebar.

Tip: You can link multiple issues.
🚫 Note: If only one issue is linked, it must be open for this check to pass.

Once linked, this check will pass automatically on your next push or when you re-run the workflow.

Thanks for helping maintainers! 🙌

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🚫 Missing Linked Issue

Hi 👋 This pull request does not appear to be linked to any open issue yet.

Linking your PR to an issue helps keep the project tidy and ensures the issue is closed automatically.

✔️ How to fix this

  • Add a keyword like Fixes #123 or Closes #456 to your PR description or a commit message.
  • Or link it manually using the "Linked issues" panel in the PR sidebar.

Tip: You can link multiple issues.
🚫 Note: If only one issue is linked, it must be open for this check to pass.

Once linked, this check will pass automatically on your next push or when you re-run the workflow.

Thanks for helping maintainers! 🙌

}
| None => {
let endpoint = ApiEndpoint.getApiEndPoint(~publishableKey)
PaymentHelpersV2.fetchPaymentManagementList(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have vault details in this API

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support separate card fields (cardNumber, cardExpiry, cardCvc) as independently mountable elements

4 participants