From abe9d6e0a0e38f87939c5cb7c67312667d5aed66 Mon Sep 17 00:00:00 2001 From: ArushKapoorJuspay <121166031+ArushKapoorJuspay@users.noreply.github.com> Date: Mon, 6 May 2024 14:10:23 +0530 Subject: [PATCH 1/2] refactor: moved Card Number, Cvc and Expiry to Dynamic Fields for Card Payment method (#282) Co-authored-by: Praful Koppalkar <126236898+prafulkoppalkar@users.noreply.github.com> --- src/Components/DynamicFields.res | 20 ++- src/Payment.res | 11 +- src/Payments/CardPayment.res | 215 ++------------------------ src/Payments/PaymentMethodsRecord.res | 64 +++++--- src/Utilities/DynamicFieldsUtils.res | 75 ++++++++- src/Utilities/PaymentBody.res | 33 +++- 6 files changed, 168 insertions(+), 250 deletions(-) diff --git a/src/Components/DynamicFields.res b/src/Components/DynamicFields.res index ca508ff05..c356df40c 100644 --- a/src/Components/DynamicFields.res +++ b/src/Components/DynamicFields.res @@ -59,7 +59,7 @@ let make = ( ~isAllStoredCardsHaveName, (), ) - ->DynamicFieldsUtils.updateDynamicFields(billingAddress, ()) + ->DynamicFieldsUtils.updateDynamicFields(billingAddress, ~list, ~paymentMethod, ~isSavedCardFlow, ()) ->Belt.SortArray.stableSortBy(PaymentMethodsRecord.sortPaymentMethodFields) //<...>// }, (requiredFields, isAllStoredCardsHaveName, isSavedCardFlow)) @@ -149,7 +149,7 @@ let make = ( cardRef, icon, cardError, - _, + setCardError, maxCardLength, ) = switch cardProps { | Some(cardProps) => cardProps @@ -165,7 +165,7 @@ let make = ( expiryRef, _, expiryError, - _, + setExpiryError, ) = switch expiryProps { | Some(expiryProps) => expiryProps | None => defaultExpiryProps @@ -181,7 +181,7 @@ let make = ( cvcRef, _, cvcError, - _, + setCvcError, ) = switch cvcProps { | Some(cvcProps) => cvcProps | None => defaultCvcProps @@ -276,9 +276,17 @@ let make = ( ~isSavedCardFlow, ~isAllStoredCardsHaveName, ~setRequiredFieldsBody, + ~list, ) - let submitCallback = DynamicFieldsUtils.useSubmitCallback() + let submitCallback = DynamicFieldsUtils.useSubmitCallback( + ~cardNumber, + ~setCardError, + ~cardExpiry, + ~setExpiryError, + ~cvcNumber, + ~setCvcError, + ) useSubmitPaymentData(submitCallback) let bottomElement = @@ -326,7 +334,7 @@ let make = ( key={`outside-billing-${index->Int.toString}`} className="flex flex-col w-full place-content-between" style={ReactDOMStyle.make( - ~marginTop=index !== 0 || paymentMethod === "card" ? themeObj.spacingGridColumn : "", + ~marginTop=index !== 0 ? themeObj.spacingGridColumn : "", ~gridColumnGap=themeObj.spacingGridRow, (), )}> diff --git a/src/Payment.res b/src/Payment.res index 8d15eec44..94b14daee 100644 --- a/src/Payment.res +++ b/src/Payment.res @@ -228,13 +228,6 @@ let make = (~paymentMode, ~integrateError, ~logger) => { checkCardExpiry(getCardElementValue(iframeId, "card-expiry")) | _ => true } - let cardNetwork = { - if cardBrand != "" { - [("card_network", cardNumber->CardUtils.getCardBrand->JSON.Encode.string)] - } else { - [] - } - } if validFormat { let body = switch paymentMode->getPaymentMode { | Card => @@ -246,7 +239,7 @@ let make = (~paymentMode, ~integrateError, ~logger) => { ~year, ~cardHolderName="", ~cvcNumber, - ~cardBrand=cardNetwork, + ~cardBrand, (), ) | CardNumberElement => @@ -258,7 +251,7 @@ let make = (~paymentMode, ~integrateError, ~logger) => { ~year, ~cardHolderName="", ~cvcNumber=localCvcNumber, - ~cardBrand=cardNetwork, + ~cardBrand, (), ) | _ => [] diff --git a/src/Payments/CardPayment.res b/src/Payments/CardPayment.res index 5fb573243..c9c95b2ee 100644 --- a/src/Payments/CardPayment.res +++ b/src/Payments/CardPayment.res @@ -15,58 +15,21 @@ let make = ( open Utils open UtilityHooks - let {config, themeObj, localeString} = Recoil.useRecoilValueFromAtom(RecoilAtoms.configAtom) - let {innerLayout} = config.appearance + let {themeObj, localeString} = Recoil.useRecoilValueFromAtom(RecoilAtoms.configAtom) let options = Recoil.useRecoilValueFromAtom(RecoilAtoms.optionAtom) let loggerState = Recoil.useRecoilValueFromAtom(RecoilAtoms.loggerAtom) let (nickname, setNickname) = React.useState(_ => "") - let ( - isCardValid, - setIsCardValid, - cardNumber, - changeCardNumber, - handleCardBlur, - cardRef, - icon, - cardError, - setCardError, - maxCardLength, - ) = cardProps + let (_, _, cardNumber, _, _, _, _, _, _, _) = cardProps let cardBrand = React.useMemo(() => { cardNumber->CardUtils.getCardBrand }, [cardNumber]) - let ( - isExpiryValid, - setIsExpiryValid, - cardExpiry, - changeCardExpiry, - handleExpiryBlur, - expiryRef, - _, - expiryError, - setExpiryError, - ) = expiryProps - - let ( - isCVCValid, - setIsCVCValid, - cvcNumber, - _, - changeCVCNumber, - handleCVCBlur, - cvcRef, - _, - cvcError, - setCvcError, - ) = cvcProps let {displaySavedPaymentMethodsCheckbox} = Recoil.useRecoilValueFromAtom(RecoilAtoms.optionAtom) let intent = PaymentHelpers.usePaymentIntent(Some(loggerState), Card) let showFields = Recoil.useRecoilValueFromAtom(RecoilAtoms.showCardFieldsAtom) - let setComplete = Recoil.useSetRecoilState(RecoilAtoms.fieldsComplete) let (isSaveCardsChecked, setIsSaveCardsChecked) = React.useState(_ => false) let setUserError = message => { @@ -76,23 +39,15 @@ let make = ( let (requiredFieldsBody, setRequiredFieldsBody) = React.useState(_ => Dict.make()) let areRequiredFieldsValid = Recoil.useRecoilValueFromAtom(RecoilAtoms.areRequiredFieldsValid) + let areRequiredFieldsEmpty = Recoil.useRecoilValueFromAtom(RecoilAtoms.areRequiredFieldsEmpty) - let complete = isAllValid(isCardValid, isCVCValid, isExpiryValid, true, "payment") - let empty = cardNumber == "" || cardExpiry == "" || cvcNumber == "" - React.useEffect(() => { - setComplete(_ => complete) - None - }, [complete]) - - useHandlePostMessages(~complete=complete && areRequiredFieldsValid, ~empty, ~paymentType="card") + useHandlePostMessages( + ~complete=areRequiredFieldsValid, + ~empty=areRequiredFieldsEmpty, + ~paymentType="card", + ) let isGuestCustomer = useIsGuestCustomer() - let isCvcValidValue = CardUtils.getBoolOptionVal(isCVCValid) - let (cardEmpty, cardComplete, cardInvalid) = CardUtils.useCardDetails( - ~cvcNumber, - ~isCVCValid, - ~isCvcValidValue, - ) let isCustomerAcceptanceRequired = useIsCustomerAcceptanceRequired( ~displaySavedPaymentMethodsCheckbox, @@ -104,26 +59,9 @@ let make = ( let submitCallback = React.useCallback((ev: Window.event) => { let json = ev.data->JSON.parseExn let confirm = json->getDictFromJson->ConfirmType.itemToObjMapper - let (month, year) = CardUtils.getExpiryDates(cardExpiry) let onSessionBody = [("customer_acceptance", PaymentBody.customerAcceptanceBody)] - let cardNetwork = { - if cardBrand != "" { - [("card_network", cardBrand->JSON.Encode.string)] - } else { - [] - } - } - let defaultCardBody = PaymentBody.cardPaymentBody( - ~cardNumber, - ~month, - ~year, - ~cardHolderName="", - ~cvcNumber, - ~cardBrand=cardNetwork, - ~nickname, - (), - ) + let defaultCardBody = PaymentBody.dynamicCardPaymentBody(~cardBrand, ~nickname, ()) let banContactBody = PaymentBody.bancontactBody() let cardBody = if isCustomerAcceptanceRequired { defaultCardBody->Array.concat(onSessionBody) @@ -131,11 +69,7 @@ let make = ( defaultCardBody } if confirm.doSubmit { - let validFormat = - (isBancontact || - (isCVCValid->Option.getOr(false) && - isCardValid->Option.getOr(false) && - isExpiryValid->Option.getOr(false))) && areRequiredFieldsValid + let validFormat = areRequiredFieldsValid if validFormat && (showFields || isBancontact) { intent( ~bodyArr={ @@ -150,32 +84,11 @@ let make = ( ~handleUserError=false, (), ) - } else { - if cardNumber === "" { - setCardError(_ => localeString.cardNumberEmptyText) - setUserError(localeString.enterFieldsText) - } - if cardExpiry === "" { - setExpiryError(_ => localeString.cardExpiryDateEmptyText) - setUserError(localeString.enterFieldsText) - } - if !isBancontact && cvcNumber === "" { - setCvcError(_ => localeString.cvcNumberEmptyText) - setUserError(localeString.enterFieldsText) - } - if !validFormat { - setUserError(localeString.enterValidDetailsText) - } + } else if !validFormat { + setUserError(localeString.enterValidDetailsText) } } - }, ( - areRequiredFieldsValid, - requiredFieldsBody, - empty, - complete, - isCustomerAcceptanceRequired, - nickname, - )) + }, (areRequiredFieldsValid, requiredFieldsBody, isCustomerAcceptanceRequired, nickname)) useSubmitPaymentData(submitCallback) let paymentMethod = isBancontact ? "bank_redirect" : "card" @@ -189,114 +102,12 @@ let make = ( let nicknameFieldClassName = conditionsForShowingSaveCardCheckbox ? "pt-2" : "pt-5" - let compressedLayoutStyleForCvcError = - innerLayout === Compressed && cvcError->String.length > 0 ? "!border-l-0" : "" -
- -
- {React.string(localeString.cardHeader)} -
-
- - String.length > 0 - ? "border-b-0" - : ""} - /> -
-
- -
-
- -
-
- String.length > 0 || - cvcError->String.length > 0 || - expiryError->String.length > 0}> -
- {React.string("Invalid input")} -
-
-
{ - switch (str, isBancontact) { - | ("user_email_address", _) => Email - | ("user_full_name", _) => FullName - | ("user_country", _) => Country - | ("user_bank", _) => Bank - | ("user_phone_number", _) => PhoneNumber - | ("user_address_line1", _) => AddressLine1 - | ("user_address_line2", _) => AddressLine2 - | ("user_address_city", _) => AddressCity - | ("user_address_pincode", _) => AddressPincode - | ("user_address_state", _) => AddressState - | ("user_blik_code", _) => BlikCode - | ("user_billing_name", _) => BillingName - | ("user_card_number", true) => CardNumber - | ("user_card_expiry_month", true) => CardExpiryMonth - | ("user_card_expiry_year", true) => CardExpiryYear - | ("user_card_cvc", true) => CardCvc +let getPaymentMethodsFieldTypeFromString = str => { + switch str { + | "user_email_address" => Email + | "user_full_name" => FullName + | "user_country" => Country + | "user_bank" => Bank + | "user_phone_number" => PhoneNumber + | "user_address_line1" => AddressLine1 + | "user_address_line2" => AddressLine2 + | "user_address_city" => AddressCity + | "user_address_pincode" => AddressPincode + | "user_address_state" => AddressState + | "user_blik_code" => BlikCode + | "user_billing_name" => BillingName + | "user_card_number" => CardNumber + | "user_card_expiry_month" => CardExpiryMonth + | "user_card_expiry_year" => CardExpiryYear + | "user_card_cvc" => CardCvc | _ => None } } @@ -558,7 +558,7 @@ let getPaymentMethodsFieldTypeFromDict = dict => { } } -let getFieldType = (dict, isBancontact) => { +let getFieldType = dict => { let fieldClass = dict ->Dict.get("field_type") @@ -570,7 +570,7 @@ let getFieldType = (dict, isBancontact) => { None | Number(_val) => None | Array(_arr) => None - | String(val) => val->getPaymentMethodsFieldTypeFromString(isBancontact) + | String(val) => val->getPaymentMethodsFieldTypeFromString | Object(dict) => dict->getPaymentMethodsFieldTypeFromDict } } @@ -614,6 +614,16 @@ let getIsAnyBillingDetailEmpty = (requiredFields: array) => { }) } +let filterCardDetailsFromSavedPaymentMethod = fieldType => { + switch fieldType { + | CardNumber + | CardExpiryMonth + | CardExpiryYear + | CardCvc => true + | _ => false + } +} + let getPaymentMethodFields = ( paymentMethod, requiredFields: array, @@ -631,6 +641,10 @@ let getPaymentMethodFields = ( isAllStoredCardsHaveName ) { None + } else if ( + isSavedCardFlow && requiredField.field_type->filterCardDetailsFromSavedPaymentMethod + ) { + None } else { requiredField.field_type } @@ -862,7 +876,7 @@ let getAchConnectors = (dict, str) => { ->getStrArray("elligible_connectors") } -let getDynamicFieldsFromJsonDict = (dict, isBancontact) => { +let getDynamicFieldsFromJsonDict = dict => { let requiredFields = Utils.getJsonFromDict(dict, "required_fields", JSON.Encode.null) ->Utils.getDictFromJson @@ -873,7 +887,7 @@ let getDynamicFieldsFromJsonDict = (dict, isBancontact) => { { required_field: requiredFieldsDict->Utils.getString("required_field", ""), display_name: requiredFieldsDict->Utils.getString("display_name", ""), - field_type: requiredFieldsDict->getFieldType(isBancontact), + field_type: requiredFieldsDict->getFieldType, value: requiredFieldsDict->Utils.getString("value", ""), } }) @@ -894,9 +908,7 @@ let getPaymentMethodTypes = (dict, str) => { bank_names: getBankNames(jsonDict, "bank_names"), bank_debits_connectors: getAchConnectors(jsonDict, "bank_debit"), bank_transfers_connectors: getAchConnectors(jsonDict, "bank_transfer"), - required_fields: jsonDict->getDynamicFieldsFromJsonDict( - paymentMethodType === "bancontact_card", - ), + required_fields: jsonDict->getDynamicFieldsFromJsonDict, surcharge_details: jsonDict->getSurchargeDetails, } }) @@ -1055,3 +1067,5 @@ let paymentMethodFieldToStrMapper = (field: paymentMethodsFields) => { | CardExpiryAndCvc => "CardExpiryAndCvc" } } + +let cardDetailsFields = [CardNumber, CardExpiryMonth, CardExpiryYear, CardCvc] diff --git a/src/Utilities/DynamicFieldsUtils.res b/src/Utilities/DynamicFieldsUtils.res index f4a77f9d9..f0fabe294 100644 --- a/src/Utilities/DynamicFieldsUtils.res +++ b/src/Utilities/DynamicFieldsUtils.res @@ -52,6 +52,16 @@ let getBillingAddressPathFromFieldType = (fieldType: PaymentMethodsRecord.paymen } } +let getCardAddressPathFromFieldType = (fieldType: PaymentMethodsRecord.paymentMethodsFields) => { + switch fieldType { + | CardNumber => "payment_method_data.card.card_number" + | CardExpiryMonth => "payment_method_data.card.card_exp_month" + | CardExpiryYear => "payment_method_data.card.card_exp_year" + | CardCvc => "payment_method_data.card.card_cvc" + | _ => "" + } +} + let removeBillingDetailsIfUseBillingAddress = ( requiredFields: array, billingAddress: PaymentType.billingAddress, @@ -144,7 +154,6 @@ let useRequiredFieldsEmptyAndValid = ( | StateAndCity => state.value !== "" && city.value !== "" | CountryAndPincode(countryArr) => (country !== "" || countryArr->Array.length === 0) && postalCode.value !== "" - | AddressCity => city.value !== "" | AddressPincode => postalCode.value !== "" | AddressState => state.value !== "" @@ -377,6 +386,7 @@ let useRequiredFieldsBody = ( ~isSavedCardFlow, ~isAllStoredCardsHaveName, ~setRequiredFieldsBody, + ~list: PaymentMethodsRecord.list, ) => { let email = Recoil.useRecoilValueFromAtom(userEmailAddress) let fullName = Recoil.useRecoilValueFromAtom(userFullName) @@ -464,6 +474,23 @@ let useRequiredFieldsBody = ( } } + let addCardDetailsBodyIfFallback = requiredFieldsBody => { + if ( + (paymentMethodType === "debit" || paymentMethodType === "credit") && + list.payment_methods->Array.length === 0 && + !isSavedCardFlow + ) { + PaymentMethodsRecord.cardDetailsFields->Array.reduce(requiredFieldsBody, (acc, item) => { + let value = item->getFieldValueFromFieldType + let path = item->getCardAddressPathFromFieldType + acc->Dict.set(path, value->JSON.Encode.string) + acc + }) + } else { + requiredFieldsBody + } + } + React.useEffect(() => { let requiredFieldsBody = requiredFields @@ -492,6 +519,7 @@ let useRequiredFieldsBody = ( acc }) ->addBillingDetailsIfUseBillingAddress + ->addCardDetailsBodyIfFallback setRequiredFieldsBody(_ => requiredFieldsBody) None @@ -618,26 +646,50 @@ let combineCardExpiryAndCvc = arr => { } } +let addCardDetailsIfFallback = ( + fieldsArr, + ~list: PaymentMethodsRecord.list, + ~paymentMethod, + ~isSavedCardFlow, +) => { + if paymentMethod === "card" && list.payment_methods->Array.length === 0 && !isSavedCardFlow { + fieldsArr->Array.concat(PaymentMethodsRecord.cardDetailsFields) + } else { + fieldsArr + } +} + let updateDynamicFields = ( arr: array, billingAddress, + ~list: PaymentMethodsRecord.list, + ~paymentMethod, + ~isSavedCardFlow, (), ) => { arr ->Utils.removeDuplicate ->Array.filter(item => item !== None) ->addBillingAddressIfUseBillingAddress(billingAddress) + ->addCardDetailsIfFallback(~list, ~paymentMethod, ~isSavedCardFlow) ->combineStateAndCity ->combineCountryAndPostal ->combineCardExpiryMonthAndYear ->combineCardExpiryAndCvc } -let useSubmitCallback = () => { - let logger = Recoil.useRecoilValueFromAtom(loggerAtom) - let (line1, setLine1) = Recoil.useLoggedRecoilState(userAddressline1, "line1", logger) - let (line2, setLine2) = Recoil.useLoggedRecoilState(userAddressline2, "line2", logger) - let (state, setState) = Recoil.useLoggedRecoilState(userAddressState, "state", logger) +let useSubmitCallback = ( + ~cardNumber, + ~setCardError, + ~cardExpiry, + ~setExpiryError, + ~cvcNumber, + ~setCvcError, +) => { + let logger = Recoil.useRecoilValueFromAtom(RecoilAtoms.loggerAtom) + let (line1, setLine1) = Recoil.useLoggedRecoilState(RecoilAtoms.userAddressline1, "line1", logger) + let (line2, setLine2) = Recoil.useLoggedRecoilState(RecoilAtoms.userAddressline2, "line2", logger) + let (state, setState) = Recoil.useLoggedRecoilState(RecoilAtoms.userAddressState, "state", logger) let (postalCode, setPostalCode) = Recoil.useLoggedRecoilState( userAddressPincode, "postal_code", @@ -682,6 +734,15 @@ let useSubmitCallback = () => { errorString: localeString.cityEmptyText, }) } + if cardNumber === "" { + setCardError(_ => localeString.cardNumberEmptyText) + } + if cardExpiry === "" { + setExpiryError(_ => localeString.cardExpiryDateEmptyText) + } + if cvcNumber === "" { + setCvcError(_ => localeString.cvcNumberEmptyText) + } } - }, (line1, line2, state, city, postalCode)) + }, (line1, line2, state, city, postalCode, cardNumber, cardExpiry, cvcNumber)) } diff --git a/src/Utilities/PaymentBody.res b/src/Utilities/PaymentBody.res index d3b7ab42d..e6f75bcea 100644 --- a/src/Utilities/PaymentBody.res +++ b/src/Utilities/PaymentBody.res @@ -58,17 +58,48 @@ let cardPaymentBody = ( cardBody->Array.push(("nick_name", nickname->JSON.Encode.string))->ignore } + if cardBrand != "" { + cardBody->Array.push(("card_network", cardBrand->JSON.Encode.string))->ignore + } + [ ("payment_method", "card"->JSON.Encode.string), ( "payment_method_data", - [("card", cardBody->Array.concat(cardBrand)->Dict.fromArray->JSON.Encode.object)] + [("card", cardBody->Dict.fromArray->JSON.Encode.object)] ->Dict.fromArray ->JSON.Encode.object, ), ] } +let dynamicCardPaymentBody = (~cardBrand, ~nickname="", ()) => { + let cardBody = [] + + if nickname != "" { + cardBody->Array.push(("nick_name", nickname->JSON.Encode.string))->ignore + } + + if cardBrand != "" { + cardBody->Array.push(("card_network", cardBrand->JSON.Encode.string))->ignore + } + + let paymentMethodData = if cardBody->Array.length > 0 { + [ + ( + "payment_method_data", + [("card", cardBody->Dict.fromArray->JSON.Encode.object)] + ->Dict.fromArray + ->JSON.Encode.object, + ), + ] + } else { + [] + } + + [("payment_method", "card"->JSON.Encode.string)]->Array.concat(paymentMethodData) +} + let bancontactBody = () => [ ("payment_method", "bank_redirect"->JSON.Encode.string), ("payment_method_type", "bancontact_card"->JSON.Encode.string), From 763f9c51b806c2f855965b0ab21ed3ffea589ba4 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 6 May 2024 08:42:10 +0000 Subject: [PATCH 2/2] chore(release): 0.50.1 [skip ci] ## [0.50.1](https://github.com/juspay/hyperswitch-web/compare/v0.50.0...v0.50.1) (2024-05-06) --- CHANGELOG.md | 2 ++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d63c4a0dc..7c9d5c4df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +## [0.50.1](https://github.com/juspay/hyperswitch-web/compare/v0.50.0...v0.50.1) (2024-05-06) + # [0.50.0](https://github.com/juspay/hyperswitch-web/compare/v0.49.2...v0.50.0) (2024-05-06) diff --git a/package-lock.json b/package-lock.json index 7ece27014..daaa7785c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "orca-payment-page", - "version": "0.50.0", + "version": "0.50.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "orca-payment-page", - "version": "0.50.0", + "version": "0.50.1", "hasInstallScript": true, "dependencies": { "@aws-sdk/client-cloudfront": "^3.414.0", diff --git a/package.json b/package.json index e50563545..1e1fa5612 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "orca-payment-page", - "version": "0.50.0", + "version": "0.50.1", "main": "index.js", "private": true, "dependencies": {