Skip to content
110 changes: 76 additions & 34 deletions src/Components/SavedMethods.res
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,12 @@ let make = (
loggerState.setLogError(~value=message, ~eventName=INVALID_FORMAT)
}
let (isSaveCardsChecked, setIsSaveCardsChecked) = React.useState(_ => false)
let {displaySavedPaymentMethodsCheckbox, readOnly} = Recoil.useRecoilValueFromAtom(
let (showMore, setShowMore) = React.useState(_ => true)
let {displaySavedPaymentMethodsCheckbox, readOnly, layout} = Recoil.useRecoilValueFromAtom(
RecoilAtoms.optionAtom,
)
let layoutClass = CardUtils.getLayoutClass(layout)
let displayMergedSavedMethods = layoutClass.savedMethodsLayout.displayMergedSavedMethods
let isGuestCustomer = useIsGuestCustomer()

let {iframeId, clientSecret} = Recoil.useRecoilValueFromAtom(RecoilAtoms.keys)
Expand All @@ -66,38 +69,67 @@ let make = (
let paymentMethodListValue = Recoil.useRecoilValueFromAtom(PaymentUtils.paymentMethodListValue)
let {paymentToken: paymentTokenVal, customerId} = paymentToken

let (cardOptionDetails, dropDownOptionsDetails) = React.useMemo(() => {
(
savedMethods->Array.slice(~start=0, ~end=layoutClass.savedMethodsLayout.maxSavedItems),
savedMethods->Array.sliceToEnd(~start=layoutClass.savedMethodsLayout.maxSavedItems),
)
}, [savedMethods])

let renderSavedCards = (cardsArr: array<PaymentType.customerMethods>) => {
cardsArr
->Array.mapWithIndex((obj, i) =>
<SavedCardItem
key={i->Int.toString}
setPaymentToken
isActive={paymentTokenVal == obj.paymentToken}
paymentItem=obj
brandIcon={obj->getPaymentMethodBrand}
index=i
savedCardlength
cvcProps
setRequiredFieldsBody
/>
)
->React.array
}

let clickToPayElement = {
<RenderIf condition={clickToPayConfig.isReady == Some(true)}>
<ClickToPayAuthenticate
loggerState
savedMethods
isClickToPayAuthenticateError
setIsClickToPayAuthenticateError
setPaymentToken
paymentTokenVal
cvcProps
getVisaCards
setIsClickToPayRememberMe
closeComponentIfSavedMethodsAreEmpty
/>
</RenderIf>
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be a react component


let mergedViewBottomElement = {
<div
className="PickerItemContainer" tabIndex={0} role="region" ariaLabel="Saved payment methods">
{renderSavedCards(cardOptionDetails)}
<div
className={`transition-all duration-300 ease-in-out overflow-hidden
${showMore ? "max-h-0 opacity-0" : "max-h-screen opacity-100"}
`}>
{renderSavedCards(dropDownOptionsDetails)}
</div>
clickToPayElement
</div>
}

let bottomElement = {
<div
className="PickerItemContainer" tabIndex={0} role="region" ariaLabel="Saved payment methods">
{savedMethods
->Array.mapWithIndex((obj, i) =>
<SavedCardItem
key={i->Int.toString}
setPaymentToken
isActive={paymentTokenVal == obj.paymentToken}
paymentItem=obj
brandIcon={obj->getPaymentMethodBrand}
index=i
savedCardlength
cvcProps
setRequiredFieldsBody
/>
)
->React.array}
<RenderIf condition={clickToPayConfig.isReady == Some(true)}>
<ClickToPayAuthenticate
loggerState
savedMethods
isClickToPayAuthenticateError
setIsClickToPayAuthenticateError
setPaymentToken
paymentTokenVal
cvcProps
getVisaCards
setIsClickToPayRememberMe
closeComponentIfSavedMethodsAreEmpty
/>
</RenderIf>
<RenderIf condition={!displayMergedSavedMethods}> {renderSavedCards(savedMethods)} </RenderIf>
clickToPayElement
Copy link
Contributor

Choose a reason for hiding this comment

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

Why these are not functional components?

</div>
}

Expand Down Expand Up @@ -336,14 +368,23 @@ let make = (
let enableSavedPaymentShimmer = React.useMemo(() => {
savedCardlength === 0 &&
!showPaymentMethodsScreen &&
(loadSavedCards === PaymentType.LoadingSavedCards || clickToPayConfig.isReady->Option.isNone)
}, (savedCardlength, loadSavedCards, showPaymentMethodsScreen, clickToPayConfig.isReady))
(loadSavedCards === PaymentType.LoadingSavedCards || clickToPayConfig.isReady->Option.isNone) &&
!displayMergedSavedMethods
}, (
savedCardlength,
loadSavedCards,
showPaymentMethodsScreen,
clickToPayConfig.isReady,
displayMergedSavedMethods,
))
Comment on lines 436 to +447
Copy link
Contributor

Choose a reason for hiding this comment

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

Try to simplify this condition for better readability.


<div className="flex flex-col overflow-auto h-auto no-scrollbar animate-slowShow">
{if enableSavedPaymentShimmer {
<PaymentElementShimmer.SavedPaymentCardShimmer />
} else {
<RenderIf condition={!showPaymentMethodsScreen}> {bottomElement} </RenderIf>
<RenderIf condition={!showPaymentMethodsScreen}>
{displayMergedSavedMethods ? mergedViewBottomElement : bottomElement}
</RenderIf>
}}
Copy link
Contributor

Choose a reason for hiding this comment

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

Try this

<RenderIf condition={enableSavedPaymentShimmer}>
  <PaymentElementShimmer.SavedPaymentCardShimmer />
</RenderIf>

<RenderIf condition={!enableSavedPaymentShimmer && !showPaymentMethodsScreen}>
  {displayMergedSavedMethods ? mergedViewBottomElement : bottomElement}
</RenderIf>

<RenderIf condition={conditionsForShowingSaveCardCheckbox}>
<div className="pt-4 pb-2 flex items-center justify-start">
Expand All @@ -360,7 +401,7 @@ let make = (
}
/>
</RenderIf>
<RenderIf condition={!enableSavedPaymentShimmer}>
<RenderIf condition={!enableSavedPaymentShimmer && !displayMergedSavedMethods}>
<div
className="Label flex flex-row gap-3 items-end cursor-pointer mt-4"
style={
Expand All @@ -386,5 +427,6 @@ let make = (
{React.string(localeString.morePaymentMethods)}
</div>
</RenderIf>
<ShowMoreButton displayMergedSavedMethods dropDownOptionsDetails showMore setShowMore />
</div>
}
24 changes: 24 additions & 0 deletions src/Components/ShowMoreButton.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@react.component
let make = (~displayMergedSavedMethods, ~dropDownOptionsDetails, ~showMore, ~setShowMore) => {
let {themeObj} = Recoil.useRecoilValueFromAtom(RecoilAtoms.configAtom)

<RenderIf condition={displayMergedSavedMethods && dropDownOptionsDetails->Array.length > 0}>
<div
className="Label flex flex-row gap-1 items-end cursor-pointer mt-3"
style={
fontSize: "14px",
float: "left",
fontWeight: "500",
width: "fit-content",
color: themeObj.colorPrimary,
}
Copy link
Contributor

Choose a reason for hiding this comment

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

why inlining styling is required here?

onClick={_ => {
setShowMore(_ => !showMore)
}}>
Copy link
Contributor

@PritishBudhiraja PritishBudhiraja Sep 8, 2025

Choose a reason for hiding this comment

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

Suggested change
onClick={_ => {
setShowMore(_ => !showMore)
}}>
onClick={_ => setShowMore(prev => !prev) }>

{showMore ? React.string("Show more") : React.string("Show Less")}
<div className="m-1">
{showMore ? <Icon name="arrow-down" size=10 /> : <Icon name="arrow-up" size=10 />}
</div>
</div>
</RenderIf>
}
35 changes: 30 additions & 5 deletions src/PaymentElement.res
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ let make = (~cardProps, ~expiryProps, ~cvcProps, ~paymentType: CardThemeType.mod
}, (clickToPayConfig, isClickToPayAuthenticateError))

let layoutClass = CardUtils.getLayoutClass(layout)
let displayMergedSavedMethods = layoutClass.savedMethodsLayout.displayMergedSavedMethods

let (getVisaCards, closeComponentIfSavedMethodsAreEmpty) = ClickToPayHook.useClickToPay(
~areClickToPayUIScriptsLoaded,
Expand Down Expand Up @@ -278,6 +279,11 @@ let make = (~cardProps, ~expiryProps, ~cvcProps, ~paymentType: CardThemeType.mod
}, [selectedOption])
useSubmitPaymentData(submitCallback)
React.useEffect(() => {
if displayMergedSavedMethods && selectedOption == "saved_methods" {
setShowPaymentMethodsScreen(_ => false)
} else if displayMergedSavedMethods {
setShowPaymentMethodsScreen(_ => true)
}
Copy link
Contributor

@Sanskar2001 Sanskar2001 Sep 4, 2025

Choose a reason for hiding this comment

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

Suggested change
if displayMergedSavedMethods && selectedOption == "saved_methods" {
setShowPaymentMethodsScreen(_ => false)
} else if displayMergedSavedMethods {
setShowPaymentMethodsScreen(_ => true)
}
let showScreen =
displayMergedSavedMethods && selectedOption !== "saved_methods";
setShowPaymentMethodsScreen(showScreen);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This would not be equivalent to the previous case

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added this

if displayMergedSavedMethods {
  setShowPaymentMethodsScreen(_ => selectedOption != "saved_methods")
}

setSelectedOption(prev =>
selectedOption !== ""
? prev
Expand Down Expand Up @@ -402,6 +408,19 @@ let make = (~cardProps, ~expiryProps, ~cvcProps, ~paymentType: CardThemeType.mod
</RenderIf>
}}
</SessionPaymentWrapper>
| SavedMethods =>
<SavedMethods
paymentToken
setPaymentToken
savedMethods
loadSavedCards
cvcProps
sessions
isClickToPayAuthenticateError
setIsClickToPayAuthenticateError
getVisaCards
closeComponentIfSavedMethodsAreEmpty
/>
| _ =>
<ReusableReactSuspense loaderComponent={loader()} componentName="PaymentMethodsWrapperLazy">
<PaymentMethodsWrapperLazy paymentMethodName=selectedOption />
Expand Down Expand Up @@ -450,15 +469,18 @@ let make = (~cardProps, ~expiryProps, ~cvcProps, ~paymentType: CardThemeType.mod
</div>
</RenderIf>
{if clickToPayConfig.isReady->Option.isNone {
if areClickToPayUIScriptsLoaded {
if displayMergedSavedMethods {
<PaymentElementShimmer />
} else if areClickToPayUIScriptsLoaded {
<ClickToPayHelpers.SrcLoader />
} else {
<PaymentElementShimmer.SavedPaymentCardShimmer />
}
} else {
<RenderIf
condition={!showPaymentMethodsScreen &&
(displaySavedPaymentMethods || isShowPaymentMethodsDependingOnClickToPay)}>
(displaySavedPaymentMethods || isShowPaymentMethodsDependingOnClickToPay) &&
!displayMergedSavedMethods}>
<SavedMethods
paymentToken
setPaymentToken
Expand All @@ -475,7 +497,7 @@ let make = (~cardProps, ~expiryProps, ~cvcProps, ~paymentType: CardThemeType.mod
}}
<RenderIf
condition={(paymentOptions->Array.length > 0 || walletOptions->Array.length > 0) &&
showPaymentMethodsScreen &&
(showPaymentMethodsScreen || displayMergedSavedMethods) &&
clickToPayConfig.isReady->Option.isSome}>
<div
className="flex flex-col place-items-center"
Expand Down Expand Up @@ -510,7 +532,9 @@ let make = (~cardProps, ~expiryProps, ~cvcProps, ~paymentType: CardThemeType.mod
</RenderIf>
<RenderIf
condition={((displaySavedPaymentMethods && savedMethods->Array.length > 0) ||
isShowPaymentMethodsDependingOnClickToPay) && showPaymentMethodsScreen}>
isShowPaymentMethodsDependingOnClickToPay) &&
Copy link
Contributor

Choose a reason for hiding this comment

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

can we simplify this? It's too many conditions inside a prop - not readable

showPaymentMethodsScreen &&
!displayMergedSavedMethods}>
<div
className="Label flex flex-row gap-3 items-end cursor-pointer mt-4"
style={
Expand Down Expand Up @@ -542,7 +566,8 @@ let make = (~cardProps, ~expiryProps, ~cvcProps, ~paymentType: CardThemeType.mod
</RenderIf>
| _ =>
<RenderIf
condition={!displaySavedPaymentMethods &&
condition={(!displaySavedPaymentMethods ||
Copy link
Contributor

Choose a reason for hiding this comment

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

can we simplify this? It's too many conditions inside a prop - not readable

(displayMergedSavedMethods && savedMethods->Array.length == 0)) &&
paymentOptions->Array.length == 0 &&
walletOptions->Array.length == 0}>
<PaymentElementShimmer />
Expand Down
1 change: 1 addition & 0 deletions src/PaymentElementV2.res
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ let make = (~cardProps, ~expiryProps, ~cvcProps, ~paymentType: CardThemeType.mod
| Boleto
| PayPal
| EFT
| SavedMethods
| Unknown => React.null
}}
</ErrorBoundary>
Expand Down
7 changes: 7 additions & 0 deletions src/Payments/PaymentMethodsRecord.res
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,13 @@ let getPaymentMethodsFields = (~localeString: LocaleStringTypes.localeStrings) =
displayName: localeString.payment_methods_card,
miniIcon: None,
},
{
paymentMethodName: "saved_methods",
icon: Some(icon("default-card", ~size=19)),
fields: [],
displayName: "Saved",
miniIcon: None,
},
{
paymentMethodName: "klarna",
icon: Some(icon("klarna", ~size=19)),
Expand Down
3 changes: 3 additions & 0 deletions src/Types/PaymentModeType.res
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type payment =
| Boleto
| PayPal
| EFT
| SavedMethods
| Unknown

let paymentMode = str => {
Expand Down Expand Up @@ -57,11 +58,13 @@ let paymentMode = str => {
| "boleto" => Boleto
| "paypal" => PayPal
| "eft" => EFT
| "saved_methods" => SavedMethods
| _ => Unknown
}
}

let defaultOrder = [
"saved_methods",
"card",
"apple_pay",
"google_pay",
Expand Down
37 changes: 36 additions & 1 deletion src/Types/PaymentType.res
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,16 @@ type wallets = {
style: style,
}
type business = {name: string}
type savedMethodsLayout = {
maxSavedItems: int,
displayMergedSavedMethods: bool,
}
type layoutConfig = {
defaultCollapsed: bool,
radios: bool,
spacedAccordionItems: bool,
maxAccordionItems: int,
savedMethodsLayout: savedMethodsLayout,
\"type": layout,
}

Expand Down Expand Up @@ -249,11 +254,16 @@ let defaultCustomerMethods = {
recurringEnabled: false,
billing: defaultDisplayBillingDetails,
}
let defaultSavedMethodsLayout = {
maxSavedItems: 4,
displayMergedSavedMethods: false,
}
let defaultLayout = {
defaultCollapsed: false,
radios: false,
spacedAccordionItems: false,
maxAccordionItems: 4,
savedMethodsLayout: defaultSavedMethodsLayout,
\"type": Tabs,
}
let defaultAddress: address = {
Expand Down Expand Up @@ -659,14 +669,38 @@ let getFields: (Dict.t<JSON.t>, string, 'a) => fields = (dict, str, logger) => {
})
->Option.getOr(defaultFields)
}
let getMergedViewValues = (json, logger) => {
let dict = json->Utils.getDictFromDict("savedMethodsLayout")
unknownKeysWarning(
["maxSavedItems", "displayMergedSavedMethods"],
dict,
"options.layout.savedMethodsLayout",
)
{
maxSavedItems: getNumberWithWarning(dict, "maxSavedItems", 4, ~logger),
displayMergedSavedMethods: getBoolWithWarning(
dict,
"displayMergedSavedMethods",
false,
~logger,
),
}
}
let getLayoutValues = (val, logger) => {
switch val->JSON.Classify.classify {
| String(str) => StringLayout(str->getLayout)
| Object(json) =>
ObjectLayout({
let layoutType = getWarningString(json, "type", "tabs", ~logger)
unknownKeysWarning(
["defaultCollapsed", "radios", "spacedAccordionItems", "type", "maxAccordionItems"],
[
"defaultCollapsed",
"radios",
"spacedAccordionItems",
"type",
"maxAccordionItems",
"savedMethodsLayout",
],
json,
"options.layout",
)
Expand All @@ -675,6 +709,7 @@ let getLayoutValues = (val, logger) => {
radios: getBoolWithWarning(json, "radios", false, ~logger),
spacedAccordionItems: getBoolWithWarning(json, "spacedAccordionItems", false, ~logger),
maxAccordionItems: getNumberWithWarning(json, "maxAccordionItems", 4, ~logger),
savedMethodsLayout: getMergedViewValues(json, logger),
\"type": layoutType->getLayout,
}
})
Expand Down
Loading