Skip to content

feat: superposition dynamic fields#1608

Open
Shivam25092001 wants to merge 6 commits into
mainfrom
feat-superposition-dynamic-fields-clean
Open

feat: superposition dynamic fields#1608
Shivam25092001 wants to merge 6 commits into
mainfrom
feat-superposition-dynamic-fields-clean

Conversation

@Shivam25092001

@Shivam25092001 Shivam25092001 commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Type of Change

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

Description

  • DynamicFields.res — Rewritten to use useConfigurationServicegetSuperpositionFinalFields; RFF <Form> wraps all fields; billing split is path-based; card fields filtered at source to avoid double-render with CardPayment
  • DynamicFields/ — New per-field components: CardHolderNameField, StateDropdownField, EmailField, CountryDropdownField, PhoneField, etc.
  • DateOfBirth.res — Refactored as RFF-wired DateField; this component was only getting used by DynamicFields
  • package.json — Added react-final-form.
  • ReactFinalForm owns all field state, validation, and initial values.

Architectural Decisions

  • No backward compatibility with the old paymentMethodsFields enum pipeline. setRequiredFieldsBody contract with all consumers is unchanged — they still receive Dict.t<JSON.t> keyed by API paths.
  • RFF owns all field state, validation, and initial values. useFormStateHandler syncs formProps.valuessetRequiredFieldsBody and formProps.validareRequiredFieldsValid.

Why include RFF change in this PR?

  • Using react-final-forms, the I/O based on fieldConfigs from superposition becomes very readable and easier to manage.
  • Same change-radius.

References

Base PRs

Stacked PRs

How did you test it?

SCREEN RECORDING

Screen.Recording.2026-05-26.at.12.54.06.PM.mov

Issue

Fixes #1518

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 Jun 14, 2026

Copy link
Copy Markdown

Review changes with  SemanticDiff

Changed Files
File Status
  package-lock.json  0% smaller
  package.json  0% smaller
  src/Components/CryptoCurrencyNetworks.res Unsupported file format
  src/Components/DynamicFieldInput.res Unsupported file format
  src/Components/DynamicFields.res Unsupported file format
  src/Components/DynamicFields/CardHolderNameField.res Unsupported file format
  src/Components/DynamicFields/CountryDropdownField.res Unsupported file format
  src/Components/DynamicFields/EmailField.res Unsupported file format
  src/Components/DynamicFields/GenericDropdownField.res Unsupported file format
  src/Components/DynamicFields/GenericInputField.res Unsupported file format
  src/Components/DynamicFields/PhoneCountryCodeDropdownField.res Unsupported file format
  src/Components/DynamicFields/PhoneField.res Unsupported file format
  src/Components/DynamicFields/StateDropdownField.res Unsupported file format
  src/LoaderController.res Unsupported file format
  src/Payments/DateOfBirth.res Unsupported file format
  src/Utilities/DynamicFieldsUtils.res Unsupported file format
  src/Utilities/PaymentUtils.res Unsupported file format
  src/Utilities/Utils.res Unsupported file format

@github-actions

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! 🙌

@XyneSpaces

Copy link
Copy Markdown

Code Review: Feat superposition dynamic fields clean

Classification

Feature: Superposition Dynamic Fields with React Final Form integration
Risk Level: High (form state management library addition, payment-critical code paths)
Files Changed: 21 files (+1976/-1085 lines)


Architecture Overview

This PR introduces React Final Form (RFF) for form state management in the dynamic fields system, significantly refactoring how form inputs handle state, validation, and submission.

Area Change
Dependencies Adds react-final-form@^7.0.0
Component Structure Extracts 8 new field components from monolithic DynamicFields.res
State Management Migrates from Recoil-only to RFF form state + Recoil for persistence
Validation Centralized field validators in DynamicFieldsUtils.res

Tier 1 — Payment Correctness & Safety

✅ No Silent-Null onFailure Patterns

Checked new field components — no error-swallowing patterns detected in the added/modified files.

✅ No Card Data in Logs

No console.log statements with card-related fields found in diff.

⚠️ Form State Boundary Risk in LoaderController.res

+230/-12 lines in the SDK entry point. Need to verify:

  1. Form initialization doesn't lose payment intent state during recovery
  2. Browser refresh behavior preserved for saved payment methods

⚠️ Dependency Addition — React Final Form

Adding a new form state library requires scrutiny:

  • Bundle size increase: final-form@5.0.1 (~4KB gzipped) + react-final-form@7.0.0 (~3KB gzipped)
  • Peer dependency on @babel/runtime
  • Form state lifecycle must align with SDK's mount/unmount patterns

Action needed: Document bundle size impact and verify RFF unmounts properly when SDK is destroyed.


Tier 2 — Parity, Routing, Release Hygiene

✅ v1/v2 Parity

Dynamic fields changes are in shared components — should apply to both v1 and v2.

⚠️ Submodule Coordination Required

DynamicFieldsUtils.res changes affect field validation logic. Verify if hyperswitch-client-core has equivalent dynamic field logic needing updates.

⚠️ Empty PR Body

The PR description is blank. Please add:

  • What superposition dynamic fields are
  • Why React Final Form was chosen over alternatives
  • Migration plan for existing integrations

Tier 3 — Style & Implementation Details

💡 Good Code Organization

Extracting field components to DynamicFields/ subdirectory is a maintainability win with clear single-responsibility boundaries.

💡 Type Safety Improvement in DynamicFieldsUtils.res

Using LocaleStringTypes.toValidationLocale instead of Obj.magic eliminates runtime coercion risks.

⚠️ Large Change in PaymentUtils.res

+366/-75 lines affecting payment payload construction. Needs verification that confirm payload structure matches backend expectations.


Test Recommendations

  1. Matrix testing — All payment methods using dynamic fields (cards with billing, BNPL, bank transfers)
  2. Edge cases — Form reset after failed payment, prefilled customer data, browser autofill
  3. Bundle analysis — Verify RFF addition doesn't exceed size budgets

Summary

Item Status
Silent-null patterns ✅ None found
Card data exposure ✅ None found
3DS impact ✅ N/A
Bundle size documented ⚠️ Needs documentation
PR description ⚠️ Empty - needs content
Client-core coordination ⚠️ Verify needed
LoaderController risk ⚠️ Careful testing required

Verdict: ⚠️ Request Changes — This substantial refactoring improves organization but needs more documentation, bundle analysis, and cross-repo verification before merge.

@XyneSpaces XyneSpaces left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review Summary

Classification: Major refactoring — Migrates DynamicFields from Recoil-based state to React Final Form (RFF)
Risk Level: Medium (large-scale form architecture change, ~2000 lines modified)
Scope: Dynamic field rendering system, form validation, state management


Findings Overview

Severity Count Issues
🚨 Critical 0 None identified
⚠️ Warning 2 Empty PR description, missing test checklist items
💡 Suggestion 2 Bundle size impact, error boundary consideration

⚠️ Warnings

1. Incomplete PR Description

The PR body has empty sections for Description and Testing. For a change of this magnitude (+187/-783 in DynamicFields.res alone), the description should document:

  • Migration rationale from Recoil to RFF
  • Backward compatibility plan for existing field types
  • Risk assessment for form validation edge cases

2. Checklist Items Not Completed

The PR checklist shows these items unchecked:

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

Please confirm npm run re:build passes and code has been reviewed. Unit tests for the new field components would strengthen confidence in this refactor.


💡 Suggestions

1. Monitor bundle size impact of react-final-form

The new dependency react-final-form (v7.0.0) brings in final-form (v5.0.1) as a peer dependency (~20KB minified). Verify this doesn't materially impact the SDK bundle size budget.

2. Consider error boundary for field component crashes

Individual field components now encapsulate more logic. Consider wrapping <DynamicFieldInput.makeRow /> calls in an error boundary to prevent a single field misconfiguration from crashing the entire payment form.


Tier 1 Verification Results

Check Status Notes
Silent-null onFailure ✅ Pass No new error-swallowing patterns in diff
Bare silent catch ✅ Pass No catch blocks that collapse to null
Card data in logs ✅ Pass No console logging of sensitive fields
v1/v2 parity ℹ️ N/A Appears to be form-layer only, no PaymentBody changes
Endpoint routing ✅ Pass No new URL literals

Tier 2 Verification Results

Check Status Notes
Submodule pointer moves ✅ Pass No sdk-utils version changes
Promote to shared-utils ✅ Pass New components are web-specific (RFF-dependent)
Version sync ℹ️ N/A No version bump in package.json

Positive Notes

  • ✅ Clean separation of concerns with new DynamicFields/ subdirectory
  • ✅ Good use of RFF useField hook pattern across all new components
  • ✅ Proper validation integration via DynamicFieldsUtils.resolveValidator
  • GenericInputField uses RFF <Field> render prop pattern correctly
  • DateOfBirth refactored to integrate with RFF validation
  • LoaderController properly guards sdkConfigsValue update behind error check

Pre-existing Issues (unchanged)

None introduced by this PR.


Overall Assessment:
This is a well-structured migration to React Final Form that should improve form state management consistency. The risk is moderate due to the large footprint. Completing the PR description and checklist would help reviewers understand the full scope.

Verdict: 💬 Comment — Clean refactor. Please complete PR description and checklist before merge.

@XyneSpaces XyneSpaces left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review Summary

Verdict: 🔄 Request Changes

🚨 1 critical · ⚠️ 2 warnings · 💡 2 suggestions

This is a substantial architectural change introducing React Final Form across the dynamic fields system. The dependency addition and state management changes warrant careful review of error handling and backward compatibility.

Classification: core-flow-framework · secondary: sdk-ffi-codegen
Risk: high

@XyneSpaces XyneSpaces left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ React Final Form dependency addition requires peer dependency verification

react-final-form v7.0.0 pulls in final-form v5.0.1 as a peer. The RFF v7 line requires React 16.8+. Verify compatibility with the project's React 18.2.0 and ensure no duplicate final-form instances in the bundle.

Fix: Confirm npm ls final-form produces a single instance.

Comment thread package.json
@@ -12,6 +12,7 @@
"react": "^18.2.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ New dependency react-final-form@^7.0.0 - verify no duplicate final-form peer dependency in bundle.

Comment thread package-lock.json
@@ -17,6 +17,7 @@
"react": "^18.2.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Ensure @babel/runtime@7.29.2 addition doesn't conflict with existing babel runtime versions.

@Shivam25092001 Shivam25092001 changed the title Feat superposition dynamic fields clean feat: superposition dynamic fields Jun 15, 2026
@github-actions github-actions Bot removed the S-conventions-not-followed Some Github conventions not followed label Jun 15, 2026
@@ -0,0 +1,24 @@
open SuperpositionTypes

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.

merge this component to GenericInputField and use switch on fieldType to render

open SuperpositionTypes

// "John Doe" -> ("John", "Doe") "John" -> ("John", "")
let splitName = (value: string): (string, string) =>

@aritro2002 aritro2002 Jun 15, 2026

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.

check if we can use this function getFirstAndLastNameFromFullName instead of creating splitName

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.

"John Doe" (two spaces) would produce ("John", " Doe") with a leading space in the last name. This case will be missed by splitName

stateField.input.onChange(effectiveCode)
}
None
}, [effectiveCode])

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.

Incomplete useEffect Dependency Array

Suggested change
}, [effectiveCode])
}, [hasStates, storedCode, effectiveCode])

Comment on lines +34 to +38
let (name, setName) = React.useState(() => {
let first = firstProps.input.value->Option.getOr("")
let last = lastProps.input.value->Option.getOr("")
[first, last]->Array.filter(val => val !== "")->Array.join(" ")
})

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.

This only runs ONCE at mount. If firstProps/lastProps aren't populated yet (e.g. async form init, or values come in after mount), this will be "" forever.

Comment thread src/LoaderController.res
| _ => ()
}
setSdkConfigs(_ => updatedState)
if !isSdkConfigsError {

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.

why is there need of isSdkConfigsError, if no response comes, we can set the default value, right?


let validate = DynamicFieldsUtils.resolveValidator(~field=fieldConfig, ~localeObject=localeString)

let field = ReactFinalForm.useField(path, ~config={validate: validate})

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.

use <ReactFinalForm.Field> instead of useField

allCardHolderNameFields->Array.get(0)->Option.map(field => field.confirmRequestWritePath)
<RenderIf condition={firstCardHolderNamePath === Some(field.confirmRequestWritePath)}>
<CardHolderNameField fields=allCardHolderNameFields />
</RenderIf>

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.

render a single component here and move the rendering logic inside the react component

| CardHolderName => <CardHolderNameField ... />

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.

do same for all other cases

value
onChange={ev => {
let val = ReactEvent.Form.target(ev)["value"]
fields->Array.forEach(field => form.change(field.confirmRequestWritePath, val))

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.

primaryField is not mapped with onChange, update primaryField value here so that ReactFinalForms can properly track field state

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.

 <PaymentInputField
        fieldName={label}
        value
        onChange={ev => {
          let val: string = ReactEvent.Form.target(ev)["value"]
          // Drive the registered field through its own handler...
          primaryField.input.onChange(val)

          secondaryPaths->Array.forEach(path => form.change(path, val))
        }}
        onBlur={_ev => primaryField.input.onBlur()}
        isValid
        errorString
        placeholder
        inputRef={fieldRef}
        autocomplete
      />

->Option.getOr(Dict.make())
->getArray("countries")

let phoneNumberCodeOptions: array<DropdownField.optionType> = countryAndCodeList->Array.reduce(

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.

this value is getting recomputed on every render, please use useMemo here

let {label} = DynamicFieldsUtils.resolveFieldTexts(~field=fieldConfig, ~localeObject=localeString)
let validate = DynamicFieldsUtils.resolveValidator(~field=fieldConfig, ~localeObject=localeString)

let countryAndCodeList =

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.

useMemo()

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.

Superposition in web-sdk

4 participants