Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/common/CustomAccordionView.res
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ let make = (
~onAllCollapsed: bool => unit=_ => (),
) => {
let (nativeProp, _) = React.useContext(NativePropContext.nativePropContext)
let (accountPaymentMethodData, customerPaymentMethodData, _) = React.useContext(
let (accountPaymentMethodData, customerPaymentMethodData, _, _) = React.useContext(
AllApiDataContextNew.allApiDataContext,
)
let layout = nativeProp.configuration.paymentMethodLayout
Expand Down
30 changes: 0 additions & 30 deletions src/components/dynamic/AddressElement.res

This file was deleted.

73 changes: 48 additions & 25 deletions src/components/dynamic/CardElement.res
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ module CardBrandAndScanCardIcon = {
}
}

let useOptionalCardField = (
config: option<SuperpositionTypes.fieldConfig>,
~sentinel,
~validate,
) => {
let present = config->Option.isSome
let path = config->Option.mapOr(sentinel, c => c.confirmRequestWritePath)
let {input, meta} = ReactFinalForm.useField(path, ~config={validate: ?validate})
(present, input, meta)
}

@react.component
let make = (
~fields: array<SuperpositionTypes.fieldConfig>,
Expand All @@ -39,20 +50,16 @@ let make = (
~accessible=?,
~checkEligibility: option<string> => unit=_ => (),
) => {
switch (
fields->Array.get(0),
fields->Array.get(1),
fields->Array.get(2),
fields->Array.get(3),
fields->Array.get(4),
) {
| (
Some(cardNumberConfig),
Some(cardExpiryMonthConfig),
Some(cardExpiryYearConfig),
Some(cardCvcConfig),
Some(cardNetworkConfig),
) => {
let findField = (renderType: SuperpositionTypes.fieldType) =>
fields->Array.find((f: SuperpositionTypes.fieldConfig) => f.fieldRenderType === renderType)
let cardNumberConfig = findField(SuperpositionTypes.CardNumber)
let cardExpiryMonthConfig = findField(SuperpositionTypes.CardExpiryMonth)
let cardExpiryYearConfig = findField(SuperpositionTypes.CardExpiryYear)
let cardCvcConfig = findField(SuperpositionTypes.Cvc)
let cardNetworkConfig = findField(SuperpositionTypes.CardNetwork)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Shouldn't fields controlled by priority?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

  • Previously, the CardElement logic relied on the order and priority of fields in the array (for example, assuming fields[0] was card number, fields[1] was expiry month, etc.). This approach was fragile because any change in field priority ordering it fell through to | _ => React.null
  • To make the rendering logic more robust and independent of field ordering, we have extended fieldRenderType with dedicated values such as CardExpiryMonth, CardExpiryYear, and CardNetwork. CardElement and other Special UX components now identify and consume fields based on fieldRenderType rather than array position or priority. Since fieldRenderType is a stable, explicit, and non-changing identifier, this ensures the components receive the correct fields regardless of ordering changes.


switch (cardNumberConfig, cardExpiryMonthConfig, cardExpiryYearConfig) {
| (Some(cardNumberConfig), Some(cardExpiryMonthConfig), Some(cardExpiryYearConfig)) => {
let (nativeProp, _) = React.useContext(NativePropContext.nativePropContext)
let {eligibilityStatus} = React.useContext(DynamicFieldsContext.dynamicFieldsContext)
let emitter = PaymentEvents.usePaymentEventEmitter()
Expand All @@ -73,31 +80,41 @@ let make = (
let nullRef = React.useRef(Nullable.null)

let {input: cardNumberInput, meta: cardNumberMeta} = ReactFinalForm.useField(
cardNumberConfig.outputPath,
cardNumberConfig.confirmRequestWritePath,
~config={
validate: createFieldValidator(CardNumber),
format: formatValue(CardNumber),
},
)

let {input: cardExpiryMonthInput, meta: _cardExpiryMonthMeta} = ReactFinalForm.useField(
cardExpiryMonthConfig.outputPath,
cardExpiryMonthConfig.confirmRequestWritePath,
~config={validate: createFieldValidator(CardExpiry(expireDate))},
)

let {input: cardExpiryYearInput, meta: cardExpiryYearMeta} = ReactFinalForm.useField(
cardExpiryYearConfig.outputPath,
cardExpiryYearConfig.confirmRequestWritePath,
~config={validate: createFieldValidator(CardExpiry(expireDate))},
)

let {input: cardNetworkInput, meta: cardNetworkMeta} = ReactFinalForm.useField(
cardNetworkConfig.outputPath,
~config={validate: createFieldValidator(CardNetwork(enabledCardSchemes))},
let (_hasNetwork, cardNetworkInput, cardNetworkMeta) = useOptionalCardField(
cardNetworkConfig,
~sentinel="__card_network_unbound",
~validate={
cardNetworkConfig->Option.isSome && enabledCardSchemes->Array.length > 0
? Some(createFieldValidator(CardNetwork(enabledCardSchemes)))
: None
},
)

let {input: cardCvcInput, meta: cardCvcMeta} = ReactFinalForm.useField(
cardCvcConfig.outputPath,
~config={validate: createFieldValidator(CardCVC(cardNetworkInput.value->Option.getOr("")))},
let (hasCvc, cardCvcInput, cardCvcMeta) = useOptionalCardField(
cardCvcConfig,
~sentinel="__card_cvc_unbound",
~validate={
cardCvcConfig->Option.isSome
? Some(createFieldValidator(CardCVC(cardNetworkInput.value->Option.getOr(""))))
: None
},
)

let (
Expand Down Expand Up @@ -338,10 +355,14 @@ let make = (
(expireDate->String.length === 7 && checkCardExpiry(expireDate))}
maxLength=Some(7)
borderTopWidth=?{splitCardFields ? None : Some(borderWidth /. 2.)}
borderRightWidth=?{splitCardFields ? None : Some(borderWidth /. 2.)}
borderRightWidth=?{splitCardFields

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 are we changing this borderWidth

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

  • Because CVC is now optional (driven by the resolved config). When CVC is present, the expiry field sits to its left in the same row and shares a half-width border (borderWidth /. 2.). When CVC is hidden (hasCvc = false), expiry becomes the right-most element, so it needs the full borderWidth on its right edge; otherwise, the row's outer border renders thin. Hence Some(hasCvc ? borderWidth /. 2. : borderWidth).

? None
: Some(hasCvc ? borderWidth /. 2. : borderWidth)}
borderTopLeftRadius=?{splitCardFields ? None : Some(0.)}
borderTopRightRadius=?{splitCardFields ? None : Some(0.)}
borderBottomRightRadius=?{splitCardFields ? None : Some(0.)}
borderBottomRightRadius=?{splitCardFields
? None
: Some(hasCvc ? 0. : borderRadius)}
textColor={((cardExpiryYearMeta.error->Option.isNone ||
!cardExpiryYearMeta.touched ||
cardExpiryYearMeta.active) && expireDate->String.length < 7) ||
Expand Down Expand Up @@ -376,6 +397,7 @@ let make = (
}}
</UIUtils.RenderIf>
</View>
<UIUtils.RenderIf condition={hasCvc}>

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.

What exactly are we doing here?
Are we rendering CVC based on the superposition

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

  • Yes — CVC is config-driven now. hasCvc = findField(Cvc)->Option.isSome, so it's true only when the resolved dynamic-fields (superposition) config includes the CVC field.
  • The card block used to assume all five sub-fields always exist; now the CVC and network fields are optional. The useField for CVC is still called unconditionally via a sentinel path (Rules of Hooks), and hasCvc only gates the visual render and its validator.

<View style={s({flex: 1.})}>
<CustomInput
name={TestUtils.cvcInputTestId}
Expand Down Expand Up @@ -466,6 +488,7 @@ let make = (
}}
</UIUtils.RenderIf>
</View>
</UIUtils.RenderIf>
</View>
</View>
<UIUtils.RenderIf condition={!splitCardFields}>
Expand Down
40 changes: 31 additions & 9 deletions src/components/dynamic/CryptoElement.res
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,38 @@ let make = (
}

let {gap} = ThemebasedStyle.useThemeBasedStyle()

switch (fields->Array.get(0), fields->Array.get(1)) {
let localeObject = GetLocale.useGetLocalObj()
let getLocalized = key => GetLocale.lookupLocaleString(localeObject, key)
let currencyConfig =
fields->Array.find((f: SuperpositionTypes.fieldConfig) =>
f.fieldRenderType === SuperpositionTypes.CryptoCurrency
)
let networkConfig =
fields->Array.find((f: SuperpositionTypes.fieldConfig) =>
f.fieldRenderType === SuperpositionTypes.CryptoNetwork
)
switch (currencyConfig, networkConfig) {
| (Some(currencyConfig), Some(networkConfig)) =>
let {input: currencyInput, meta: currencyMeta} = ReactFinalForm.useField(
currencyConfig.outputPath,
~config={validate: createFieldValidator(Validation.Required)},
currencyConfig.confirmRequestWritePath,
~config={validate: createFieldValidator(Validation.Required(None))},
)

let {input: networkInput, meta: networkMeta} = ReactFinalForm.useField(
networkConfig.outputPath,
~config={validate: createFieldValidator(Validation.Required)},
networkConfig.confirmRequestWritePath,
~config={validate: createFieldValidator(Validation.Required(None))},
)

React.useEffect1(() => {
let validNetworks = getNetworkArray(currencyInput.value)
switch networkInput.value {
| Some(network) if network !== "" && !(validNetworks->Array.includes(network)) =>
networkInput.onChange("")
| _ => ()
}
None
}, [currencyInput.value->Option.getOr("")])

<>
<React.Fragment>
<View style={s({marginBottom: gap->dp})}>
Expand All @@ -53,11 +72,14 @@ let make = (
<CustomPicker
value=currencyInput.value
setValue=handlePickerChange
items={currencyConfig.options->Array.map(opt => {
items={currencyConfig.dropdownOptions->Option.getOr([])->Array.map(opt => {
SdkTypes.label: opt,
value: opt,
})}
placeholderText={GetLocale.getLocalString(currencyConfig.displayName)}
placeholderText={FieldLabelResolver.resolvePlaceholder(
currencyConfig,
getLocalized,
)}
isValid={currencyMeta.error->Option.isNone ||
!currencyMeta.touched ||
currencyMeta.active}
Expand Down Expand Up @@ -96,7 +118,7 @@ let make = (
}}
setValue=handlePickerChange
items
placeholderText={GetLocale.getLocalString(networkConfig.displayName)}
placeholderText={FieldLabelResolver.resolvePlaceholder(networkConfig, getLocalized)}
isValid={networkMeta.error->Option.isNone ||
!networkMeta.touched ||
networkMeta.active}
Expand Down
23 changes: 18 additions & 5 deletions src/components/dynamic/DateElement.res
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,10 @@ module DatePicker = {
{label: "Nov", value: "11"},
{label: "Dec", value: "12"},
]
let currentYear = Date.make()->Date.getFullYear
let yearItems = []
Belt.Range.forEach(0, 125, y => {
let yearStr = (2025 - y)->Int.toString
let yearStr = (currentYear - y)->Int.toString
yearItems->Array.push({SdkTypes.label: yearStr, value: yearStr})
})
(dayItems, monthItems, yearItems)
Expand Down Expand Up @@ -140,15 +141,27 @@ let make = (
~accessible=?,
) => {
let {gap} = ThemebasedStyle.useThemeBasedStyle()

let localeObject = GetLocale.useGetLocalObj()
let getLocalized = key => GetLocale.lookupLocaleString(localeObject, key)
fields
->Array.filter((f: SuperpositionTypes.fieldConfig) =>
switch f.fieldRenderType {
| Date | DateOfBirth => true
| _ => false
}
)
->Array.map(field => {
let placeholder = GetLocale.getLocalString(field.displayName)
let placeholder = FieldLabelResolver.resolvePlaceholder(field, getLocalized)
let validationRule = switch field.fieldRenderType {
| DateOfBirth => Validation.DateOfBirth
| _ => Validation.Required(None)
}

<React.Fragment key={field.outputPath}>
<React.Fragment key={field.confirmRequestWritePath}>
<View style={s({marginBottom: gap->dp})}>
<ReactFinalForm.Field
name=field.outputPath validate=Some(createFieldValidator(Validation.Required))>
name=field.confirmRequestWritePath
validate=Some(createFieldValidator(validationRule))>
{fieldProps => <DatePicker fieldProps placeholder accessible />}
</ReactFinalForm.Field>
</View>
Expand Down
2 changes: 1 addition & 1 deletion src/components/dynamic/DynamicComponent.res
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@react.component
let make = (~setConfirmButtonData) => {
let (nativeProp, _) = React.useContext(NativePropContext.nativePropContext)
let (accountPaymentMethodData, customerPaymentMethodData, _) = React.useContext(
let (accountPaymentMethodData, customerPaymentMethodData, _, _) = React.useContext(
AllApiDataContextNew.allApiDataContext,
)
let (viewPortContants, _) = React.useContext(ViewportContext.viewPortContext)
Expand Down
2 changes: 1 addition & 1 deletion src/components/dynamic/DynamicFields.res
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ let make = (
~checkEligibility: option<string> => unit=_ => (),
) => {
let (nativeProp, _) = React.useContext(NativePropContext.nativePropContext)
let (accountPaymentMethodData, customerPaymentMethodData, _) = React.useContext(
let (accountPaymentMethodData, customerPaymentMethodData, _, _) = React.useContext(
AllApiDataContextNew.allApiDataContext,
)
let {
Expand Down
79 changes: 79 additions & 0 deletions src/components/dynamic/FieldGrouper.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
open ParentElement

type category =
| Card
| Email
| Name
| Phone
| Crypto
| Date
| Generic

let classify = (field: SuperpositionTypes.fieldConfig): category =>
switch field.fieldRenderType {
| CardNumber | Cvc | CardExpiryMonth | CardExpiryYear | CardNetwork => Card
| Email => Email
| FirstName | LastName | CardHolderName => Name
| Phone | PhoneCountryCode => Phone
| CryptoCurrency | CryptoNetwork => Crypto
| Date | DateOfBirth => Date
| Generic | Dropdown | Country | State | LanguagePreference | BankNamesSelect => Generic
}

let toElement = (cat: category, fields): elementType =>
switch cat {
| Card => CARD(fields)
| Email => EMAIL(fields)
| Name => FULLNAME(fields)
| Phone => PHONE(fields)
| Crypto => CRYPTO(fields)
| Date => DATE(fields)
| Generic => GENERIC(fields)
}

let isCombineCluster = (cat: category) =>
switch cat {
| Card | Email | Name | Phone | Crypto => true
| Date | Generic => false
}

let groupFields = (fields: array<SuperpositionTypes.fieldConfig>): array<elementType> => {
let count = fields->Array.length
let catAt = index => fields->Array.get(index)->Option.mapOr(Generic, classify)
let firstIndexOfCat = cat => fields->Array.findIndex(f => classify(f) === cat)

let rec runEnd = (cat, index) =>
index < count && catAt(index) === cat ? runEnd(cat, index + 1) : index

let rec walk = (start, acc) =>
if start >= count {
acc
} else {
let cat = catAt(start)
if isCombineCluster(cat) {
walk(
start + 1,
start === firstIndexOfCat(cat)
? acc->Array.concat([toElement(cat, fields->Array.filter(f => classify(f) === cat))])
: acc,
)
} else {
let stop = runEnd(cat, start + 1)
walk(stop, acc->Array.concat([toElement(cat, fields->Array.slice(~start, ~end=stop))]))
}
}

walk(0, [])
}

let keyOf = (element: elementType): string =>
switch element {
| CARD(fs)
| CRYPTO(fs)
| FULLNAME(fs)
| PHONE(fs)
| EMAIL(fs)
| DATE(fs)
| GENERIC(fs) =>
fs->Array.get(0)->Option.map(f => f.confirmRequestWritePath)->Option.getOr("empty-group")
}
12 changes: 12 additions & 0 deletions src/components/dynamic/FieldLabelResolver.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
let resolvePlaceholder = (
field: SuperpositionTypes.fieldConfig,
getLocalized: string => option<string>,
) =>
switch field.merchantProvidedPlaceholderText {
| Some(text) if text !== "" => text
| _ =>
switch field.placeholderLocalizationKey {
| Some(key) => getLocalized(key)->Option.getOr(field.defaultLabelText)
| None => field.defaultLabelText
}
}
Loading
Loading