From 36b349578bd30f526fa8f27c3df0c6b9b4abbfdb Mon Sep 17 00:00:00 2001 From: Shivam Date: Sat, 21 Mar 2026 10:05:58 +0530 Subject: [PATCH 1/4] feat: superposition --- package-lock.json | 44 + package.json | 1 + shared-code | 2 +- src/Components/AddressPaymentInput.res | 252 ++- src/Components/BillingNamePaymentInput.res | 78 +- src/Components/BlikCodePaymentInput.res | 63 +- src/Components/CryptoCurrencyNetworks.res | 21 +- src/Components/DocumentNumberInput.res | 19 +- src/Components/DynamicFields.res | 1585 ++++++++++------- src/Components/EmailPaymentInput.res | 66 +- src/Components/FullNamePaymentInput.res | 117 +- src/Components/GiftCardNumberInput.res | 56 +- src/Components/GiftCardPinInput.res | 56 +- src/Components/ManageSavedItem.res | 63 +- src/Components/NicknamePaymentInput.res | 33 +- src/Components/PhoneNumberPaymentInput.res | 44 +- src/Components/PixPaymentInput.res | 132 +- src/Components/ReactFinalFormField.res | 18 + src/Components/VpaIdPaymentInput.res | 58 +- src/LocaleStrings/ArabicLocale.res | 2 + src/LocaleStrings/CatalanLocale.res | 2 + src/LocaleStrings/ChineseLocale.res | 2 + src/LocaleStrings/DeutschLocale.res | 2 + src/LocaleStrings/DutchLocale.res | 2 + src/LocaleStrings/EnglishGBLocale.res | 2 + src/LocaleStrings/EnglishLocale.res | 2 + src/LocaleStrings/FrenchBelgiumLocale.res | 2 + src/LocaleStrings/FrenchLocale.res | 2 + src/LocaleStrings/HebrewLocale.res | 2 + src/LocaleStrings/ItalianLocale.res | 2 + src/LocaleStrings/JapaneseLocale.res | 2 + src/LocaleStrings/LocaleStringTypes.res | 2 + src/LocaleStrings/PolishLocale.res | 2 + src/LocaleStrings/PortugueseLocale.res | 2 + src/LocaleStrings/RussianLocale.res | 2 + src/LocaleStrings/SpanishLocale.res | 2 + src/LocaleStrings/SwedishLocale.res | 2 + .../TraditionalChineseLocale.res | 2 + src/Payments/ACHBankDebit.res | 74 +- src/Payments/BacsBankDebit.res | 69 +- src/Payments/BankDebitModal.res | 351 ++-- src/Payments/BecsBankDebit.res | 63 +- src/Payments/CardPayment.res | 1 + src/Payments/DateOfBirth.res | 56 +- src/Payments/PaymentMethodsRecord.res | 2 +- src/Utilities/DynamicFieldsUtils.res | 1409 +++++---------- src/Utilities/Utils.res | 2 + webpack.dev.js | 18 + 48 files changed, 2374 insertions(+), 2417 deletions(-) create mode 100644 src/Components/ReactFinalFormField.res diff --git a/package-lock.json b/package-lock.json index 426fe64e5..b0108ee1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "react": "^18.2.0", "react-datepicker": "^8.4.0", "react-dom": "^18.2.0", + "react-final-form": "^7.0.0", "recoil": "^0.7.7", "webpack-merge": "^6.0.1" }, @@ -1605,6 +1606,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -5366,6 +5376,23 @@ "node": ">=8" } }, + "node_modules/final-form": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/final-form/-/final-form-5.0.0.tgz", + "integrity": "sha512-HByosvP7x3N4bWTCPoBeUeoMatadewRifxaH3qhCQI2DBwFNO0m5wxETLVUXNGWz2yokdSCMdJEvtjfZoXnqDA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.10.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/final-form" + } + }, "node_modules/finalhandler": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", @@ -8490,6 +8517,23 @@ "react": "^18.3.1" } }, + "node_modules/react-final-form": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/react-final-form/-/react-final-form-7.0.0.tgz", + "integrity": "sha512-aEeAWbSsCLVXa4GBkJtjjyhPyX4L/Pgp5P/jXZwdz0YYcK6Zs/0PkgB+qWMSyIsbbGGE7m9yYlSpui5E5Gx26A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/final-form" + }, + "peerDependencies": { + "final-form": "^5.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/package.json b/package.json index d06644e25..42c673c4b 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "react": "^18.2.0", "react-datepicker": "^8.4.0", "react-dom": "^18.2.0", + "react-final-form": "^7.0.0", "recoil": "^0.7.7", "webpack-merge": "^6.0.1" }, diff --git a/shared-code b/shared-code index 4d416578d..83c417ffc 160000 --- a/shared-code +++ b/shared-code @@ -1 +1 @@ -Subproject commit 4d416578d422dba72d82d49078a5da9df8b7c2da +Subproject commit 83c417ffca532218e15022ac3c337ac44fdcf92c diff --git a/src/Components/AddressPaymentInput.res b/src/Components/AddressPaymentInput.res index 753028e73..a8a540bdb 100644 --- a/src/Components/AddressPaymentInput.res +++ b/src/Components/AddressPaymentInput.res @@ -29,140 +29,102 @@ let showField = (val: PaymentType.addressType, type_: addressType) => { } @react.component -let make = (~className="", ~paymentType: option=?) => { +let make = ( + ~line1Path: string, + ~line2Path: string, + ~cityPath: string, + ~statePath: string, + ~countryPath: string, + ~postalPath: string, + ~className: string="", + ~paymentType: option=?, +) => { let {localeString, themeObj} = Recoil.useRecoilValueFromAtom(configAtom) let {fields} = Recoil.useRecoilValueFromAtom(optionAtom) let showDetails = getShowDetails(~billingDetails=fields.billingDetails) let contextPaymentType = usePaymentType() let paymentType = paymentType->Option.getOr(contextPaymentType) + let formState = ReactFinalForm.useFormState() + let submitFailed = formState.submitFailed - let (line1, setLine1) = Recoil.useRecoilState(userAddressline1) - let (line2, setLine2) = Recoil.useRecoilState(userAddressline2) - let (country, setCountry) = Recoil.useRecoilState(userAddressCountry) - let (city, setCity) = Recoil.useRecoilState(userAddressCity) - let (postalCode, setPostalCode) = Recoil.useRecoilState(userAddressPincode) - let (state, setState) = Recoil.useRecoilState(userAddressState) + let (showOtherFields, setShowOtherFields) = React.useState(_ => false) + + let countryData = CountryStateDataRefs.countryDataRef.contents + let countryNames = getCountryNames(countryData) + + let createValidator = rule => + Validation.createFieldValidator( + rule, + ~enabledCardSchemes=[], + ~localeObject=localeString->Obj.magic, + ) + + let line1Field: ReactFinalForm.Field.fieldProps = ReactFinalForm.useField( + line1Path, + ~config={validate: createValidator(Validation.Required)}, + ) + let line2Field: ReactFinalForm.Field.fieldProps = ReactFinalForm.useField(line2Path) + let cityField: ReactFinalForm.Field.fieldProps = ReactFinalForm.useField( + cityPath, + ~config={validate: createValidator(Validation.Required)}, + ) + let stateField: ReactFinalForm.Field.fieldProps = ReactFinalForm.useField( + statePath, + ~config={validate: createValidator(Validation.Required)}, + ) + let countryField: ReactFinalForm.Field.fieldProps = ReactFinalForm.useField(countryPath) + let postalField: ReactFinalForm.Field.fieldProps = ReactFinalForm.useField( + postalPath, + ~config={ + validate: createValidator(Validation.PostalCode(countryField.input.value->Option.getOr(""))), + }, + ) + + let stateNames = Utils.getStateNames({ + value: countryField.input.value->Option.getOr(""), + isValid: Some(true), + errorString: "", + }) let line1Ref = React.useRef(Nullable.null) let line2Ref = React.useRef(Nullable.null) let cityRef = React.useRef(Nullable.null) let postalRef = React.useRef(Nullable.null) - let (showOtherFileds, setShowOtherFields) = React.useState(_ => false) - - let stateNames = getStateNames(country) - let countryData = CountryStateDataRefs.countryDataRef.contents - let countryNames = getCountryNames(countryData) - - let checkPostalValidity = ( - postal: RecoilAtomTypes.field, - setPostal: (RecoilAtomTypes.field => RecoilAtomTypes.field) => unit, - ) => { - if postal.value !== "" { - setPostal(prev => { - ...prev, - isValid: Some(true), - errorString: "", - }) - } else { - setPostal(prev => { - ...prev, - isValid: Some(false), - errorString: localeString.postalCodeInvalidText, - }) - } - } - - let onPostalChange = ev => { - let val = ReactEvent.Form.target(ev)["value"] - setPostalCode(prev => { - ...prev, - value: val, - errorString: "", - }) - } + let hasDefaultValues = + line2Field.input.value->Option.getOr("") !== "" || + cityField.input.value->Option.getOr("") !== "" || + postalField.input.value->Option.getOr("") !== "" || + stateField.input.value->Option.getOr("") !== "" - let onPostalBlur = ev => { - let val = ReactEvent.Focus.target(ev)["value"] - if val !== "" { - setPostalCode(prev => { - ...prev, - isValid: Some(true), - errorString: "", - }) + let getErrorString = (field: ReactFinalForm.Field.fieldProps) => { + if (field.meta.touched && !field.meta.active) || submitFailed { + field.meta.error->Option.getOr("") } else { - setPostalCode(prev => { - ...prev, - isValid: Some(false), - errorString: localeString.postalCodeInvalidText, - }) + "" } } React.useEffect(() => { - checkPostalValidity(postalCode, setPostalCode) - setState(prev => { - ...prev, - value: "", - }) - + stateField.input.onChange("") None - }, [country.value]) - - let submitCallback = React.useCallback((ev: Window.event) => { - let json = ev.data->safeParse - let confirm = json->getDictFromJson->ConfirmType.itemToObjMapper - if confirm.doSubmit { - if line1.value == "" { - setLine1(prev => { - ...prev, - errorString: localeString.line1EmptyText, - }) - } - if line2.value == "" { - setLine2(prev => { - ...prev, - errorString: localeString.line2EmptyText, - }) - } - if state.value == "" { - setState(prev => { - ...prev, - errorString: localeString.stateEmptyText, - }) - } - if postalCode.value == "" { - setPostalCode(prev => { - ...prev, - errorString: localeString.postalCodeEmptyText, - }) - } - if city.value == "" { - setCity(prev => { - ...prev, - errorString: localeString.cityEmptyText, - }) - } - } - }, (line1, line2, country, state, city, postalCode)) - useSubmitPaymentData(submitCallback) - - let hasDefaulltValues = - line2.value !== "" || city.value !== "" || postalCode.value !== "" || state.value !== "" + }, [countryField.input.value])
()} + value={ + value: line1Field.input.value->Option.getOr(""), + isValid: Some(line1Field.meta.valid), + errorString: getErrorString(line1Field), + } onChange={ev => { setShowOtherFields(_ => true) - setLine1(prev => { - ...prev, - value: ReactEvent.Form.target(ev)["value"], - }) + line1Field.input.onChange(ReactEvent.Form.target(ev)["value"]) }} + onBlur={_ev => line1Field.input.onBlur()} type_="text" name="line1" className @@ -171,19 +133,21 @@ let make = (~className="", ~paymentType: option=?) => { paymentType /> - +
()} + value={ + value: line2Field.input.value->Option.getOr(""), + isValid: Some(line2Field.meta.valid), + errorString: getErrorString(line2Field), + } onChange={ev => { - setLine2(prev => { - ...prev, - value: ReactEvent.Form.target(ev)["value"], - }) + line2Field.input.onChange(ReactEvent.Form.target(ev)["value"]) }} + onBlur={_ev => line2Field.input.onBlur()} type_="text" name="line2" className @@ -196,10 +160,21 @@ let make = (~className="", ~paymentType: option=?) => { Option.getOr(""), + isValid: Some(countryField.meta.valid), + errorString: getErrorString(countryField), + } className - setValue=setCountry - options=countryNames + setValue={setter => { + let newVal = setter({ + value: countryField.input.value->Option.getOr(""), + isValid: Some(true), + errorString: "", + }) + countryField.input.onChange(newVal.value) + }} + options={countryNames} /> =?) => { stateNames->Array.length > 0}> Option.getOr(""), + isValid: Some(stateField.meta.valid), + errorString: getErrorString(stateField), + } className - setValue=setState + setValue={setter => { + let newVal = setter({ + value: stateField.input.value->Option.getOr(""), + isValid: Some(true), + errorString: "", + }) + stateField.input.onChange(newVal.value) + }} options={stateNames} /> @@ -218,15 +204,17 @@ let make = (~className="", ~paymentType: option=?) => { ()} className - value=city + value={ + value: cityField.input.value->Option.getOr(""), + isValid: Some(cityField.meta.valid), + errorString: getErrorString(cityField), + } onChange={ev => { - setCity(prev => { - ...prev, - value: ReactEvent.Form.target(ev)["value"], - }) + cityField.input.onChange(ReactEvent.Form.target(ev)["value"]) }} + onBlur={_ev => cityField.input.onBlur()} type_="text" name="city" inputRef=cityRef @@ -237,10 +225,16 @@ let make = (~className="", ~paymentType: option=?) => { ()} + value={ + value: postalField.input.value->Option.getOr(""), + isValid: Some(postalField.meta.valid), + errorString: getErrorString(postalField), + } + onBlur={_ev => postalField.input.onBlur()} + onChange={ev => { + postalField.input.onChange(ReactEvent.Form.target(ev)["value"]) + }} className name="postal" inputRef=postalRef diff --git a/src/Components/BillingNamePaymentInput.res b/src/Components/BillingNamePaymentInput.res index 3dc7796f4..08e2c5b98 100644 --- a/src/Components/BillingNamePaymentInput.res +++ b/src/Components/BillingNamePaymentInput.res @@ -1,67 +1,55 @@ open RecoilAtoms open PaymentType -open Utils @react.component -let make = (~customFieldName=None, ~requiredFields as optionalRequiredFields=?) => { +let make = ( + ~name="billingName", + ~customFieldName=None, + ~requiredFields as _optionalRequiredFields=?, +) => { let {config, localeString} = Recoil.useRecoilValueFromAtom(configAtom) let {fields} = Recoil.useRecoilValueFromAtom(optionAtom) - let (billingName, setBillingName) = Recoil.useRecoilState(userBillingName) - let showDetails = getShowDetails(~billingDetails=fields.billingDetails) - let changeName = ev => { - let val: string = ReactEvent.Form.target(ev)["value"] - setBillingName(prev => { - value: val, - isValid: Some(val !== ""), - errorString: val !== "" ? "" : prev.errorString, - }) - } - let onBlur = ev => { - let val: string = ReactEvent.Focus.target(ev)["value"] - setBillingName(prev => { - ...prev, - isValid: Some(val !== ""), - }) - } let (placeholder, fieldName) = switch customFieldName { | Some(val) => (val, val) | None => (localeString.billingNamePlaceholder, localeString.billingNameLabel) } let nameRef = React.useRef(Nullable.null) - let submitCallback = React.useCallback((ev: Window.event) => { - let json = ev.data->safeParse - let confirm = json->getDictFromJson->ConfirmType.itemToObjMapper - if confirm.doSubmit { - if billingName.value == "" { - setBillingName(prev => { - ...prev, - errorString: fieldName->localeString.nameEmptyText, - }) - } else { - switch optionalRequiredFields { - | Some(requiredFields) => - if !DynamicFieldsUtils.checkIfNameIsValid(requiredFields, BillingName, billingName) { - setBillingName(prev => { - ...prev, - errorString: fieldName->localeString.completeNameEmptyText, - }) - } - | None => () - } - } - } - }, [billingName]) - useSubmitPaymentData(submitCallback) + let createValidator = rule => + Validation.createFieldValidator( + rule, + ~enabledCardSchemes=[], + ~localeObject=localeString->Obj.magic, + ) + + let field: ReactFinalForm.Field.fieldProps = ReactFinalForm.useField( + name, + ~config={validate: createValidator(Validation.Required)}, + ) + + let billingNameValue = field.input.value->Option.getOr("") + + let changeName = ev => { + let val: string = ReactEvent.Form.target(ev)["value"] + field.input.onChange(val) + } + + let onBlur = (_ev: JsxEventU.Focus.t) => { + field.input.onBlur() + } ()} + value={ + RecoilAtomTypes.value: billingNameValue, + isValid: Some(field.meta.valid), + errorString: field.meta.touched ? field.meta.error->Option.getOr("") : "", + } onChange=changeName onBlur type_="text" diff --git a/src/Components/BlikCodePaymentInput.res b/src/Components/BlikCodePaymentInput.res index efc5a620b..30e3fc3fd 100644 --- a/src/Components/BlikCodePaymentInput.res +++ b/src/Components/BlikCodePaymentInput.res @@ -1,13 +1,20 @@ open RecoilAtoms -open Utils @react.component -let make = () => { - let (blikCode, setblikCode) = Recoil.useRecoilState(userBlikCode) +let make = (~name="blikCode") => { + let {localeString} = Recoil.useRecoilValueFromAtom(configAtom) let blikCodeRef = React.useRef(Nullable.null) + + let createValidator = rule => + Validation.createFieldValidator( + rule, + ~enabledCardSchemes=[], + ~localeObject=localeString->Obj.magic, + ) + let formatBSB = bsb => { - let formatted = bsb->String.replaceRegExp(%re("/\D+/g"), "") + let formatted = bsb->String.replaceRegExp(%re("/\\D+/g"), "") let firstPart = formatted->String.slice(~start=0, ~end=3) let secondPart = formatted->String.slice(~start=3, ~end=6) @@ -20,45 +27,33 @@ let make = () => { } } + let field: ReactFinalForm.Field.fieldProps = ReactFinalForm.useField( + name, + ~config={validate: createValidator(Validation.BlikCode)}, + ) + + let blikValue = field.input.value->Option.getOr("") + let changeblikCode = ev => { let val: string = ReactEvent.Form.target(ev)["value"] - setblikCode(prev => { - ...prev, - value: val->formatBSB, - }) + field.input.onChange(val->formatBSB) } - React.useEffect(() => { - setblikCode(prev => { - ...prev, - errorString: switch prev.isValid { - | Some(val) => val ? "" : "Invalid blikCode" - | None => "" - }, - }) - None - }, [blikCode.isValid]) - - let submitCallback = React.useCallback((ev: Window.event) => { - let json = ev.data->safeParse - let confirm = json->getDictFromJson->ConfirmType.itemToObjMapper - if confirm.doSubmit { - if blikCode.value == "" { - setblikCode(prev => { - ...prev, - errorString: "blikCode cannot be empty", - }) - } - } - }, [blikCode]) - useSubmitPaymentData(submitCallback) + let onBlur = (_ev: JsxEventU.Focus.t) => { + field.input.onBlur() + } ()} + value={ + RecoilAtomTypes.value: blikValue, + isValid: Some(field.meta.valid), + errorString: field.meta.touched ? field.meta.error->Option.getOr("") : "", + } onChange=changeblikCode + onBlur paymentType=Payment type_="blikCode" name="blikCode" diff --git a/src/Components/CryptoCurrencyNetworks.res b/src/Components/CryptoCurrencyNetworks.res index 339532361..867c96114 100644 --- a/src/Components/CryptoCurrencyNetworks.res +++ b/src/Components/CryptoCurrencyNetworks.res @@ -1,11 +1,8 @@ @react.component -let make = () => { +let make = (~name: string) => { open DropdownField let currencyVal = Recoil.useRecoilValueFromAtom(RecoilAtoms.userCurrency) let {config, localeString} = Recoil.useRecoilValueFromAtom(RecoilAtoms.configAtom) - let (cryptoCurrencyNetworks, setCryptoCurrencyNetworks) = Recoil.useRecoilState( - RecoilAtoms.cryptoCurrencyNetworks, - ) let dropdownOptions = Utils.currencyNetworksDict @@ -25,8 +22,17 @@ let make = () => { }) ).value + let field: ReactFinalForm.Field.fieldProps = ReactFinalForm.useField( + name, + ~config={ + initialValue: initialValue->JSON.Encode.string, + }, + ) + + let cryptoCurrencyNetworks = field.input.value->Option.getOr(initialValue) + React.useEffect(() => { - setCryptoCurrencyNetworks(_ => initialValue) + field.input.onChange(initialValue) None }, [initialValue]) @@ -34,7 +40,10 @@ let make = () => { appearance=config.appearance fieldName=localeString.currencyNetwork value=cryptoCurrencyNetworks - setValue=setCryptoCurrencyNetworks + setValue={setter => { + let newVal = setter(cryptoCurrencyNetworks) + field.input.onChange(newVal) + }} disabled=false options=dropdownOptions /> diff --git a/src/Components/DocumentNumberInput.res b/src/Components/DocumentNumberInput.res index 0432b18b1..eef5ac210 100644 --- a/src/Components/DocumentNumberInput.res +++ b/src/Components/DocumentNumberInput.res @@ -1,21 +1,8 @@ @react.component -let make = (~options) => { +let make = (~name: string, ~options) => { open RecoilAtoms let {config, localeString} = Recoil.useRecoilValueFromAtom(configAtom) let (documentType, setSelectedDocumentType) = Recoil.useRecoilState(RecoilAtoms.userDocumentType) - let setDocumentNumber = Recoil.useSetRecoilState(RecoilAtoms.userDocumentNumber) - - let pixCNPJ = Recoil.useRecoilValueFromAtom(userPixCNPJ) - let pixCPF = Recoil.useRecoilValueFromAtom(userPixCPF) - - React.useEffect(() => { - switch documentType { - | "cpf" => setDocumentNumber(_ => pixCPF) - | "cnpj" => setDocumentNumber(_ => pixCNPJ) - | _ => setDocumentNumber(_ => RecoilAtoms.defaultFieldValues) - } - None - }, (documentType, pixCNPJ, pixCPF))
{ width="w-40 mr-2" /> {switch documentType { - | "cpf" => - | "cnpj" => + | "cpf" => + | "cnpj" => | _ => React.null }}
diff --git a/src/Components/DynamicFields.res b/src/Components/DynamicFields.res index 3f8148e7b..0eb83d1fa 100644 --- a/src/Components/DynamicFields.res +++ b/src/Components/DynamicFields.res @@ -1,3 +1,5 @@ +open SuperpositionTypes + module DynamicFieldsToRenderWrapper = { @react.component let make = (~children, ~index, ~isInside=true) => { @@ -30,11 +32,13 @@ let make = ( ~isSaveDetailsWithClickToPay=false, ~isDisableInfoElement=false, ~isSplitPaymentsEnabled=false, + ~areCardFieldsRendered=false, ) => { open DynamicFieldsUtils open PaymentTypeContext open Utils open RecoilAtoms + let paymentMethodListValue = Recoil.useRecoilValueFromAtom(PaymentUtils.paymentMethodListValue) let paymentManagementListValue = Recoil.useRecoilValueFromAtom( PaymentUtils.paymentManagementListValue, @@ -68,109 +72,47 @@ let make = ( ~paymentMethodType, ) - let creditPaymentMethodTypes = PaymentUtils.usePaymentMethodTypeFromList( - ~paymentMethodListValue, - ~paymentMethod, - ~paymentMethodType="credit", - ) + // let creditPaymentMethodTypes = PaymentUtils.usePaymentMethodTypeFromList( + // ~paymentMethodListValue, + // ~paymentMethod, + // ~paymentMethodType="credit", + // ) - let creditPaymentMethodTypesV2 = PaymentUtilsV2.usePaymentMethodTypeFromListV2( - ~paymentsListValueV2=listValue, + // let creditPaymentMethodTypesV2 = PaymentUtilsV2.usePaymentMethodTypeFromListV2( + // ~paymentsListValueV2=listValue, + // ~paymentMethod, + // ~paymentMethodType="credit", + // ) + + let (missingRequiredFields, initialValues, _) = useSuperpositionFields( ~paymentMethod, - ~paymentMethodType="credit", + ~paymentMethodType, + ~paymentMethodTypes, + ~paymentMethodListValue, ) - let requiredFieldsWithBillingDetails = React.useMemo(() => { - if paymentMethod === "card" { - switch GlobalVars.sdkVersion { - | V2 => - let creditRequiredFields = - listValue.paymentMethodsEnabled - ->Array.filter(item => { - item.paymentMethodSubtype === "credit" && item.paymentMethodType === "card" - }) - ->Array.get(0) - ->Option.getOr(UnifiedHelpersV2.defaultPaymentMethods) - - let finalCreditRequiredFields = creditRequiredFields.requiredFields - [ - ...paymentMethodTypes.required_fields, - ...finalCreditRequiredFields, - ]->removeRequiredFieldsDuplicates - - | V1 => - let creditRequiredFields = creditPaymentMethodTypes.required_fields - - [ - ...paymentMethodTypes.required_fields, - ...creditRequiredFields, - ]->removeRequiredFieldsDuplicates - } - } else if dynamicFieldsEnabledPaymentMethods->Array.includes(paymentMethodType) { - switch GlobalVars.sdkVersion { - | V1 => paymentMethodTypes.required_fields - | V2 => paymentMethodTypesV2.requiredFields - } - } else { - [] - } - }, ( - paymentMethod, - paymentMethodTypes.required_fields, - paymentMethodTypesV2.requiredFields, - paymentMethodType, - creditPaymentMethodTypes.required_fields, - creditPaymentMethodTypesV2.requiredFields, - )) - - let requiredFields = React.useMemo(() => { - requiredFieldsWithBillingDetails - ->removeBillingDetailsIfUseBillingAddress(billingAddress) - ->removeClickToPayFieldsIfSaveDetailsWithClickToPay(isSaveDetailsWithClickToPay) - }, (requiredFieldsWithBillingDetails, isSaveDetailsWithClickToPay)) - - let isAllStoredCardsHaveName = React.useMemo(() => { - PaymentType.getIsStoredPaymentMethodHasName(savedMethod) - }, [savedMethod]) + let processedFieldConfigs = React.useMemo(() => { + missingRequiredFields + ->removeBillingDetailsFromFieldConfigs(billingAddress) + ->removeClickToPayFieldsFromFieldConfigs(isSaveDetailsWithClickToPay) + ->removeCardFieldsFromFieldConfigs(areCardFieldsRendered) + ->processFieldConfigs(billingAddress, isSaveDetailsWithClickToPay) + }, (missingRequiredFields, billingAddress, isSaveDetailsWithClickToPay, areCardFieldsRendered)) - //<...>// - - let clickToPayConfig = Recoil.useRecoilValueFromAtom(RecoilAtoms.clickToPayConfig) - - let fieldsArr = React.useMemo(() => { - PaymentMethodsRecord.getPaymentMethodFields( - paymentMethodType, - requiredFields, - ~isSavedCardFlow, - ~isAllStoredCardsHaveName, - ~localeString, - ) - ->updateDynamicFields(billingAddress, isSaveDetailsWithClickToPay, clickToPayConfig) - ->Belt.SortArray.stableSortBy(PaymentMethodsRecord.sortPaymentMethodFields) - //<...>// - }, (requiredFields, isAllStoredCardsHaveName, isSavedCardFlow, isSaveDetailsWithClickToPay)) + // let clickToPayConfig = Recoil.useRecoilValueFromAtom(RecoilAtoms.clickToPayConfig) let isSpacedInnerLayout = config.appearance.innerLayout === Spaced - let (line1, setLine1) = Recoil.useRecoilState(userAddressline1) - let (line2, setLine2) = Recoil.useRecoilState(userAddressline2) - let (city, setCity) = Recoil.useRecoilState(userAddressCity) - let (state, setState) = Recoil.useRecoilState(userAddressState) - let (postalCode, setPostalCode) = Recoil.useRecoilState(userAddressPincode) - let (currency, setCurrency) = Recoil.useRecoilState(userCurrency) let line1Ref = React.useRef(Nullable.null) let line2Ref = React.useRef(Nullable.null) let cityRef = React.useRef(Nullable.null) + let stateRef = React.useRef(Nullable.null) let bankAccountNumberRef = React.useRef(Nullable.null) let sourceBankAccountIdRef = React.useRef(Nullable.null) let postalRef = React.useRef(Nullable.null) let (selectedBank, setSelectedBank) = Recoil.useRecoilState(userBank) let (country, setCountry) = Recoil.useRecoilState(userCountry) - - let (bankAccountNumber, setBankAccountNumber) = Recoil.useRecoilState(userBankAccountNumber) - let (sourceBankAccountId, setSourceBankAccountId) = Recoil.useRecoilState(sourceBankAccountId) - let countryList = CountryStateDataRefs.countryDataRef.contents let stateNames = getStateNames({ value: country, isValid: None, @@ -258,614 +200,919 @@ let make = ( None }) - let onPostalChange = ev => { - let val = ReactEvent.Form.target(ev)["value"] - - if val !== "" { - setPostalCode(_ => { - isValid: Some(true), - value: val, - errorString: "", - }) - } else { - setPostalCode(_ => { - isValid: Some(false), - value: val, - errorString: "", - }) + let formSubmitRef = React.useRef(None) + + let submitCallback = useSubmitCallback(~onConfirm=() => { + switch formSubmitRef.current { + | Some(submitFn) => submitFn() + | None => () } - } + }) + useSubmitPaymentData(submitCallback) - useRequiredFieldsEmptyAndValid( - ~requiredFields, - ~fieldsArr, - ~countryNames, - ~bankNames, - ~isCardValid, - ~isExpiryValid, - ~isCVCValid, - ~cardNumber, - ~cardExpiry, - ~cvcNumber, - ~isSavedCardFlow, - ~isSplitPaymentsEnabled, - ) + let bottomElement = - useSetInitialRequiredFields( - ~requiredFields={ - billingAddress.usePrefilledValues === Auto ? requiredFieldsWithBillingDetails : requiredFields - }, - ~paymentMethodType, - ) + let fullNameConfig = React.useMemo(() => { + missingRequiredFields + ->Array.find(fc => + switch fc.fieldType { + | FullNameInput(_) => true + | _ => false + } + ) + ->Option.map(fc => + switch fc.fieldType { + | FullNameInput(config) => config + | _ => {firstName: None, lastName: None} + } + ) + ->Option.getOr({firstName: None, lastName: None}) + }, [missingRequiredFields]) - useRequiredFieldsBody( - ~requiredFields, - ~paymentMethodType, - ~cardNumber, - ~cardExpiry, - ~cvcNumber, - ~isSavedCardFlow, - ~isAllStoredCardsHaveName, - ~setRequiredFieldsBody, - ) + let firstNamePath = React.useMemo(() => { + fullNameConfig.firstName->Option.map(fc => fc.outputPath)->Option.getOr("") + }, [fullNameConfig]) - let submitCallback = useSubmitCallback() - useSubmitPaymentData(submitCallback) + let lastNamePath = React.useMemo(() => { + fullNameConfig.lastName->Option.map(fc => fc.outputPath)->Option.getOr("") + }, [fullNameConfig]) - let bottomElement = + // State + City: both present → render as a side-by-side pair. + let cityOutputPath = React.useMemo(() => { + processedFieldConfigs->DynamicFieldsUtils.getOutputPathForFieldType(AddressCityInput) + }, [processedFieldConfigs]) - let getCustomFieldName = (item: PaymentMethodsRecord.paymentMethodsFields) => { - if ( - requiredFields - ->Array.filter(requiredFieldType => - requiredFieldType.field_type === item && - requiredFieldType.display_name === "card_holder_name" - ) - ->Array.length > 0 - ) { - Some(localeString.cardHolderName) - } else { - None - } - } + let stateOutputPath = React.useMemo(() => { + processedFieldConfigs->DynamicFieldsUtils.getOutputPathForFieldType(AddressStateInput) + }, [processedFieldConfigs]) - let dynamicFieldsToRenderOutsideBilling = React.useMemo(() => { - fieldsArr->Array.filter(isFieldTypeToRenderOutsideBilling) - }, [fieldsArr]) + let hasBothStateAndCity = React.useMemo(() => { + processedFieldConfigs->DynamicFieldsUtils.hasBothFieldTypes(AddressCityInput, AddressStateInput) + }, [processedFieldConfigs]) - let dynamicFieldsToRenderInsideBilling = React.useMemo(() => { - fieldsArr->Array.filter(field => !(field->isFieldTypeToRenderOutsideBilling)) - }, [fieldsArr]) + // Country + Postal: both present → render as a side-by-side pair. + let countryOutputPath = React.useMemo(() => { + processedFieldConfigs->DynamicFieldsUtils.getOutputPathForFieldType(AddressCountryInput) + }, [processedFieldConfigs]) - let isInfoElementPresent = dynamicFieldsToRenderOutsideBilling->Array.includes(InfoElement) + let postalOutputPath = React.useMemo(() => { + processedFieldConfigs->DynamicFieldsUtils.getOutputPathForFieldType(AddressPostalCodeInput) + }, [processedFieldConfigs]) + + let hasBothCountryAndPostal = React.useMemo(() => { + processedFieldConfigs->DynamicFieldsUtils.hasBothFieldTypes( + AddressCountryInput, + AddressPostalCodeInput, + ) + }, [processedFieldConfigs]) + + // Phone + CountryCode: both present → render as a combined phone input. + let phoneNumberOutputPath = React.useMemo(() => { + processedFieldConfigs->DynamicFieldsUtils.getOutputPathForFieldType(PhoneInput) + }, [processedFieldConfigs]) + + let countryCodeOutputPath = React.useMemo(() => { + processedFieldConfigs->DynamicFieldsUtils.getOutputPathForFieldType(CountryCodeSelect) + }, [processedFieldConfigs]) + + let hasBothPhoneAndCountryCode = React.useMemo(() => { + processedFieldConfigs->DynamicFieldsUtils.hasBothFieldTypes(PhoneInput, CountryCodeSelect) + }, [processedFieldConfigs]) + + // Month + Year expiry: both present → render as a single expiry input (outside billing). + let hasBothMonthAndYear = React.useMemo(() => { + processedFieldConfigs->DynamicFieldsUtils.hasBothFieldTypes(MonthSelect, YearSelect) + }, [processedFieldConfigs]) + + // CvcPasswordInput alongside month+year: + // render expiry + CVC side-by-side. + let hasExpiryAndCvc = React.useMemo(() => { + hasBothMonthAndYear && processedFieldConfigs->DynamicFieldsUtils.hasFieldType(CvcPasswordInput) + }, [processedFieldConfigs]) + + // Split fields into outside and inside billing sections + let fieldsOutsideBilling = React.useMemo(() => { + processedFieldConfigs->Array.filter(fc => fc->isFieldTypeToRenderOutsideBillingConfig) + }, [processedFieldConfigs]) + + let fieldsInsideBilling = React.useMemo(() => { + processedFieldConfigs->Array.filter(fc => !(fc->isFieldTypeToRenderOutsideBillingConfig)) + }, [processedFieldConfigs]) + + let isInfoElementPresent = + fieldsOutsideBilling + ->Array.find(fc => fc.fieldType == InfoElementType) + ->Option.isSome let isRenderInfoElement = isInfoElementPresent && !isDisableInfoElement - let isRenderDynamicFieldsInsideBilling = dynamicFieldsToRenderInsideBilling->Array.length > 0 + let isRenderDynamicFieldsInsideBilling = fieldsInsideBilling->Array.length > 0 let spacedStylesForBiilingDetails = isSpacedInnerLayout ? "p-2" : "my-2" - Array.length > 0}> - {<> - {dynamicFieldsToRenderOutsideBilling - ->Array.mapWithIndex((item, index) => { - Int.toString} index={index} isInside={false}> - {switch item { - | CardNumber => - - | GiftCardNumber => - | CardExpiryMonth - | CardExpiryYear - | CardExpiryMonthAndYear => - - | CardCvc => - - | GiftCardPin => - - | CardExpiryAndCvc => -
- - -
- | Currency(currencyArr) => - let updatedCurrencyArray = - currencyArr->DropdownField.updateArrayOfStringToOptionsTypeArray - - | DocumentType(opt) => { - let updatedDocumentTypeArray = - opt->DropdownField.updateArrayOfStringToOptionsTypeArrayWithUpperCaseLabel - - } - | FullName => - <> - -
- {item->getCustomFieldName->Option.getOr("")->React.string} -
-
- getCustomFieldName} - optionalRequiredFields={Some(requiredFields)} - /> - - | CryptoCurrencyNetworks => - | DateOfBirth => - | VpaId => - | PixKey => - | PixCPF => - | PixCNPJ => - | BankAccountNumber | IBAN => - { - let value = ReactEvent.Form.target(ev)["value"] - setBankAccountNumber(_ => { - isValid: Some(value !== ""), - value, - errorString: value !== "" ? "" : localeString.ibanEmptyText, - }) - }} - onBlur={ev => { - let value = ReactEvent.Focus.target(ev)["value"] - setBankAccountNumber(prev => { - ...prev, - errorString: value !== "" ? "" : localeString.ibanEmptyText, - isValid: Some(value !== ""), - }) - }} - type_="text" - name="bankAccountNumber" - maxLength=42 - inputRef=bankAccountNumberRef - placeholder="DE00 0000 0000 0000 0000 00" - /> - | SourceBankAccountId => - { - let value = ReactEvent.Form.target(ev)["value"] - setSourceBankAccountId(_ => { - isValid: Some(value !== ""), - value, - errorString: value !== "" ? "" : localeString.sourceBankAccountIdEmptyText, - }) - }} - onBlur={ev => { - let value = ReactEvent.Focus.target(ev)["value"] - setSourceBankAccountId(prev => { - ...prev, - isValid: Some(value !== ""), - }) - }} - type_="text" - name="sourceBankAccountId" - maxLength=42 - inputRef=sourceBankAccountIdRef - placeholder="DE00 0000 0000 0000 0000 00" - /> - | DocumentNumber - | Email - | InfoElement - | Country - | Bank - | None - | BillingName - | PhoneNumber - | AddressLine1 - | AddressLine2 - | AddressCity - | StateAndCity - | AddressPincode - | AddressState - | BlikCode - | SpecialField(_) - | CountryAndPincode(_) - | AddressCountry(_) - | ShippingName // Shipping Details are currently supported by only one click widgets - | ShippingAddressLine1 - | ShippingAddressLine2 - | ShippingAddressCity - | ShippingAddressPincode - | ShippingAddressState - | PhoneCountryCode - | PhoneNumberAndCountryCode - | LanguagePreference(_) - | ShippingAddressCountry(_) - | BankList(_) => React.null - }} -
- }) - ->React.array} - -
-
- {React.string(localeString.billingDetailsText)} -
-
- {dynamicFieldsToRenderInsideBilling - ->Array.mapWithIndex((item, index) => { - Int.toString} index={index}> - {switch item { - | BillingName => - | Email => - | PhoneNumberAndCountryCode => - | StateAndCity => -
- { - let value = ReactEvent.Form.target(ev)["value"] - setCity(prev => { - isValid: Some(value !== ""), - value, - errorString: value !== "" ? "" : prev.errorString, - }) - }} - onBlur={ev => { - let value = ReactEvent.Focus.target(ev)["value"] - setCity(prev => { - ...prev, - isValid: Some(value !== ""), - }) - }} - type_="text" - name="city" - inputRef=cityRef - placeholder=localeString.cityLabel - className={isSpacedInnerLayout ? "" : "!border-r-0"} + let formValidator = React.useMemo(() => { + _ => Dict.make() + }, [processedFieldConfigs]) + + let (_, setAreRequiredFieldsValid) = Recoil.useRecoilState(areRequiredFieldsValid) + + Array.length > 0}> + ()} + initialValues={Some(initialValues)} + validate={Some(formValidator)} + render={formProps => { + formSubmitRef.current = Some(formProps.form.submit) + let submitFailed = formProps.submitFailed + ReactFinalForm.useFormStateHandler( + ~onFormChange=values => { + setRequiredFieldsBody(_ => values) + }, + ~onValidationChange=isValid => { + setAreRequiredFieldsValid(_ => isValid) + }, + ~formProps, + ) + <> + {fieldsOutsideBilling + ->Array.mapWithIndex((item, index) => { + Int.toString} index={index} isInside={false}> + {switch item.fieldType { + | CardNumberTextInput => + + | GiftCardNumberInput => + | GiftCardPinInput => + | MonthSelect => + if hasExpiryAndCvc { +
+ + - Array.length > 0}> - -
- | CountryAndPincode(countryArr) => - let updatedCountryArray = - countryArr->DropdownField.updateArrayOfStringToOptionsTypeArray -
+ } else { + + } + | YearSelect => React.null + | CvcPasswordInput => + if hasExpiryAndCvc { + React.null + } else { + + } + | CurrencySelect => + let updatedCurrencyArray = + item.options->DropdownField.updateArrayOfStringToOptionsTypeArray + { + let val = field.input.value->Option.getOr(currency) { + let newVal = setter(val) + setCurrency(_ => newVal) + field.input.onChange(newVal) + }} disabled=false - options=updatedCountryArray - className={isSpacedInnerLayout ? "" : "!border-t-0 !border-r-0"} + options=updatedCurrencyArray /> + }} + /> + | DocumentTypeSelect => { + let updatedDocumentTypeArray = + item.options->DropdownField.updateArrayOfStringToOptionsTypeArrayWithUpperCaseLabel + + } + | FullNameInput(_) => + let defaultName = + paymentMethod === "card" + ? localeString.cardHolderName + : localeString.fullNameLabel + <> + +
+ {defaultName->React.string} +
+
+ + + | CryptoNetworkSelect => + | DatePicker => + | VpaTextInput => + | PixKeyInput => + | PixCpfInput => + | PixCnpjInput => + | BankAccountNumberInput => + { + let val = field.input.value->Option.getOr("") { - let value = ReactEvent.Focus.target(ev)["value"] - setPostalCode(prev => { - ...prev, - isValid: Some(value !== ""), - }) + fieldName="Account Number" + setValue={_ => ()} + value={ + RecoilAtomTypes.value: val, + isValid: Some(field.meta.valid), + errorString: submitFailed || field.meta.touched + ? field.meta.error->Option.getOr("") + : "", + } + onChange={ev => { + let rawValue = ReactEvent.Form.target(ev)["value"] + let cleanValue = rawValue->Validation.clearSpaces + field.input.onChange(cleanValue) }} - onChange=onPostalChange - name="postal" - inputRef=postalRef - placeholder=localeString.postalCodeLabel - className={isSpacedInnerLayout ? "" : "!border-t-0"} + onBlur={_ev => field.input.onBlur()} + type_="tel" + name="bankAccountNumber" + maxLength=17 + inputRef=bankAccountNumberRef + placeholder="000123456789" /> -
- | AddressLine1 => - { - let value = ReactEvent.Form.target(ev)["value"] - setLine1(prev => { - isValid: Some(value !== ""), - value, - errorString: value !== "" ? "" : prev.errorString, - }) - }} - onBlur={ev => { - let value = ReactEvent.Focus.target(ev)["value"] - setLine1(prev => { - ...prev, - isValid: Some(value !== ""), - }) - }} - type_="text" - name="line1" - inputRef=line1Ref - placeholder=localeString.line1Placeholder - className={isSpacedInnerLayout ? "" : "!border-b-0"} - /> - | AddressLine2 => - { - let value = ReactEvent.Form.target(ev)["value"] - setLine2(prev => { - isValid: Some(value !== ""), - value, - errorString: value !== "" ? "" : prev.errorString, - }) - }} - onBlur={ev => { - let value = ReactEvent.Focus.target(ev)["value"] - setLine2(prev => { - ...prev, - isValid: Some(value !== ""), - }) - }} - type_="text" - name="line2" - inputRef=line2Ref - placeholder=localeString.line2Placeholder - /> - | AddressCity => - { - let value = ReactEvent.Form.target(ev)["value"] - setCity(prev => { - isValid: Some(value !== ""), - value, - errorString: value !== "" ? "" : prev.errorString, - }) - }} - onBlur={ev => { - let value = ReactEvent.Focus.target(ev)["value"] - setCity(prev => { - ...prev, - isValid: Some(value !== ""), - }) - }} - type_="text" - name="city" - inputRef=cityRef - placeholder=localeString.cityLabel - /> - | AddressState => - Array.length > 0}> - + | IbanInput => + { + let val = field.input.value->Option.getOr("") + ()} + value={ + RecoilAtomTypes.value: val, + isValid: Some(field.meta.valid), + errorString: submitFailed || field.meta.touched + ? field.meta.error->Option.getOr("") + : "", + } + onChange={ev => field.input.onChange(ReactEvent.Form.target(ev)["value"])} + onBlur={_ev => field.input.onBlur()} + type_="text" + name="iban" + maxLength=42 + inputRef={React.useRef(Nullable.null)} + placeholder="DE00 0000 0000 0000 0000 00" /> - - | AddressPincode => - { - let value = ReactEvent.Focus.target(ev)["value"] - setPostalCode(prev => { - ...prev, - isValid: Some(value !== ""), - }) + }} + /> + | SourceBankAccountIdInput => + { + let val = field.input.value->Option.getOr("") + ()} + value={ + RecoilAtomTypes.value: val, + isValid: Some(field.meta.valid), + errorString: submitFailed || field.meta.touched + ? field.meta.error->Option.getOr("") + : "", + } + onChange={ev => field.input.onChange(ReactEvent.Form.target(ev)["value"])} + onBlur={_ev => field.input.onBlur()} + type_="text" + name="sourceBankAccountId" + maxLength=42 + inputRef=sourceBankAccountIdRef + placeholder="DE00 0000 0000 0000 0000 00" + /> + }} + /> + | DocumentNumberInput + | EmailInput + | InfoElementType + | CountrySelect + | BankSelect + | BankListSelect + | PhoneInput + | AddressLine1Input + | AddressLine2Input + | AddressCityInput + | AddressPostalCodeInput + | AddressStateInput + | BlikCodeInput + | AddressCountryInput + | // | ShippingNameInput // Shipping Details are currently supported by only one click widgets + // | ShippingAddressLine1Input + // | ShippingAddressLine2Input + // | ShippingAddressCityInput + // | ShippingAddressPostalCodeInput + // | ShippingAddressStateInput + // | ShippingAddressCountryInput + CountryCodeSelect + | TextInput + | PasswordInput + | StateSelect + | DropdownSelect => React.null + }} +
+ }) + ->React.array} + +
+
+ {React.string(localeString.billingDetailsText)} +
+
+ {fieldsInsideBilling + ->Array.mapWithIndex((item, index) => { + Int.toString} index={index}> + {switch item.fieldType { + | EmailInput => + { + let val = field.input.value->Option.getOr("") + ()} + value={ + value: val, + isValid: Some(field.meta.valid), + errorString: submitFailed || field.meta.touched + ? field.meta.error->Option.getOr("") + : "", + } + onChange={ev => + field.input.onChange(ReactEvent.Form.target(ev)["value"])} + onBlur={_ev => field.input.onBlur()} + type_="email" + name=TestUtils.emailInputTestId + inputRef={React.useRef(Nullable.null)} + placeholder="Eg: johndoe@gmail.com" + /> + }} + /> + | PhoneInput => + if hasBothPhoneAndCountryCode { + + } else { + { + let val = field.input.value->Option.getOr("") + ()} + value={ + value: val, + isValid: Some(field.meta.valid), + errorString: submitFailed || field.meta.touched + ? field.meta.error->Option.getOr("") + : "", + } + onChange={ev => + field.input.onChange(ReactEvent.Form.target(ev)["value"])} + onBlur={_ev => field.input.onBlur()} + type_="tel" + name="phone" + inputRef={React.useRef(Nullable.null)} + placeholder="000 000 000" + /> + }} + /> + } + | CountryCodeSelect => React.null + | AddressCityInput => + if hasBothStateAndCity { +
+ { + let val = field.input.value->Option.getOr("") + ()} + value={ + value: val, + isValid: Some(field.meta.valid), + errorString: submitFailed || field.meta.touched + ? field.meta.error->Option.getOr("") + : "", + } + onChange={ev => + field.input.onChange(ReactEvent.Form.target(ev)["value"])} + onBlur={_ev => field.input.onBlur()} + type_="text" + name="city" + inputRef=cityRef + placeholder=localeString.cityLabel + className={isSpacedInnerLayout ? "" : "!border-r-0"} + /> + }} + /> + { + let val = field.input.value->Option.getOr("") + if stateNames->Array.length > 0 { + Option.getOr("") + : "", + } + setValue={setter => { + let newVal = setter({ + value: val, + isValid: Some(field.meta.valid), + errorString: "", + }) + field.input.onChange(newVal.value) + }} + options={stateNames} + /> + } else { + ()} + value={ + value: val, + isValid: Some(field.meta.valid), + errorString: submitFailed || field.meta.touched + ? field.meta.error->Option.getOr("") + : "", + } + onChange={ev => + field.input.onChange(ReactEvent.Form.target(ev)["value"])} + onBlur={_ev => field.input.onBlur()} + type_="text" + name="state" + placeholder=localeString.stateLabel + inputRef=stateRef + /> + } + }} + /> +
+ } else { + { + let val = field.input.value->Option.getOr("") + ()} + value={ + value: val, + isValid: Some(field.meta.valid), + errorString: submitFailed || field.meta.touched + ? field.meta.error->Option.getOr("") + : "", + } + onChange={ev => + field.input.onChange(ReactEvent.Form.target(ev)["value"])} + onBlur={_ev => field.input.onBlur()} + type_="text" + name="city" + inputRef=cityRef + placeholder=localeString.cityLabel + /> + }} + /> + } + | AddressStateInput => + if hasBothStateAndCity { + React.null + } else { + { + let val = field.input.value->Option.getOr("") + if stateNames->Array.length > 0 { + Option.getOr("") + : "", + } + setValue={setter => { + let newVal = setter({ + value: val, + isValid: Some(field.meta.valid), + errorString: "", + }) + field.input.onChange(newVal.value) + }} + options={stateNames} + /> + } else { + ()} + value={ + value: val, + isValid: Some(field.meta.valid), + errorString: submitFailed || field.meta.touched + ? field.meta.error->Option.getOr("") + : "", + } + onChange={ev => + field.input.onChange(ReactEvent.Form.target(ev)["value"])} + onBlur={_ev => field.input.onBlur()} + type_="text" + name="state" + placeholder=localeString.stateLabel + inputRef=stateRef + /> + } + }} + /> + } + | AddressCountryInput => + if hasBothCountryAndPostal { + let updatedCountryArray = + countryNames->DropdownField.updateArrayOfStringToOptionsTypeArray +
+ { + { + let newVal = setter(field.input.value->Option.getOr(country)) + setCountry(_ => newVal) + field.input.onChange(newVal) + }} + disabled=false + options=updatedCountryArray + className={isSpacedInnerLayout ? "" : "!border-t-0 !border-r-0"} + /> + }} + /> + { + let val = field.input.value->Option.getOr("") + ()} + value={ + value: val, + isValid: Some(field.meta.valid), + errorString: submitFailed || field.meta.touched + ? field.meta.error->Option.getOr("") + : "", + } + onChange={ev => + field.input.onChange(ReactEvent.Form.target(ev)["value"])} + onBlur={_ev => field.input.onBlur()} + name="postal" + inputRef=postalRef + placeholder=localeString.postalCodeLabel + className={isSpacedInnerLayout ? "" : "!border-t-0"} + /> + }} + /> +
+ } else { + let updatedCountryArr = + countryNames->DropdownField.updateArrayOfStringToOptionsTypeArray + { + { + let newVal = setter(field.input.value->Option.getOr(country)) + setCountry(_ => newVal) + field.input.onChange(newVal) + }} + disabled=false + options=updatedCountryArr + /> + }} + /> + } + | AddressPostalCodeInput => + if hasBothCountryAndPostal { + React.null + } else { + { + let val = field.input.value->Option.getOr("") + ()} + value={ + value: val, + isValid: Some(field.meta.valid), + errorString: submitFailed || field.meta.touched + ? field.meta.error->Option.getOr("") + : "", + } + onChange={ev => + field.input.onChange(ReactEvent.Form.target(ev)["value"])} + onBlur={_ev => field.input.onBlur()} + name="postal" + inputRef=postalRef + placeholder=localeString.postalCodeLabel + /> + }} + /> + } + | AddressLine1Input => + { + let val = field.input.value->Option.getOr("") + ()} + value={ + value: val, + isValid: Some(field.meta.valid), + errorString: submitFailed || field.meta.touched + ? field.meta.error->Option.getOr("") + : "", + } + onChange={ev => + field.input.onChange(ReactEvent.Form.target(ev)["value"])} + onBlur={_ev => field.input.onBlur()} + type_="text" + name="line1" + inputRef=line1Ref + placeholder=localeString.line1Placeholder + className={isSpacedInnerLayout ? "" : "!border-b-0"} + /> + }} + /> + | AddressLine2Input => + { + let val = field.input.value->Option.getOr("") + ()} + value={ + value: val, + isValid: Some(field.meta.valid), + errorString: submitFailed || field.meta.touched + ? field.meta.error->Option.getOr("") + : "", + } + onChange={ev => + field.input.onChange(ReactEvent.Form.target(ev)["value"])} + onBlur={_ev => field.input.onBlur()} + type_="text" + name="line2" + inputRef=line2Ref + placeholder=localeString.line2Placeholder + /> + }} + /> + | BlikCodeInput => + | CountrySelect => + let updatedCountryNames = + countryNames->DropdownField.updateArrayOfStringToOptionsTypeArray + { + { + let newVal = setter(field.input.value->Option.getOr(country)) + setCountry(_ => newVal) + field.input.onChange(newVal) + }} + disabled=false + options=updatedCountryNames + /> + }} + /> + | BankListSelect => + let updatedBankNames = + Bank.getBanks(paymentMethodType) + ->getBankNames(item.options) + ->DropdownField.updateArrayOfStringToOptionsTypeArray + { + let val = field.input.value->Option.getOr(selectedBank) + { + let newVal = setter(val) + setSelectedBank(_ => newVal) + field.input.onChange(newVal) + }} + disabled=false + options=updatedBankNames + /> + }} + /> + | BankSelect => + let updatedBankNames = + bankNames->DropdownField.updateArrayOfStringToOptionsTypeArray + { + let val = field.input.value->Option.getOr(selectedBank) + { + let newVal = setter(val) + setSelectedBank(_ => newVal) + field.input.onChange(newVal) + }} + disabled=false + options=updatedBankNames + /> + }} + /> + | InfoElementType + | PixKeyInput + | PixCpfInput + | PixCnpjInput + | DocumentTypeSelect + | DocumentNumberInput + | CardNumberTextInput + | MonthSelect + | YearSelect + | CvcPasswordInput + | CurrencySelect + | FullNameInput(_) + | GiftCardNumberInput + | GiftCardPinInput + | // | ShippingNameInput // Shipping Details are currently supported by only one click widgets + // | ShippingAddressLine1Input + // | ShippingAddressLine2Input + // | ShippingAddressCityInput + // | ShippingAddressPostalCodeInput + // | ShippingAddressStateInput + // | ShippingAddressCountryInput + CryptoNetworkSelect + | DatePicker + | VpaTextInput + | BankAccountNumberInput + | IbanInput + | SourceBankAccountIdInput + | TextInput + | PasswordInput + | StateSelect + | DropdownSelect => React.null }} - onChange=onPostalChange - name="postal" - inputRef=postalRef - placeholder=localeString.postalCodeLabel - /> - | BlikCode => - | Country => - let updatedCountryNames = - countryNames->DropdownField.updateArrayOfStringToOptionsTypeArray - - | AddressCountry(countryArr) => - let updatedCountryArr = - countryArr->DropdownField.updateArrayOfStringToOptionsTypeArray - - | BankList(bankArr) => - let updatedBankNames = - Bank.getBanks(paymentMethodType) - ->getBankNames(bankArr) - ->DropdownField.updateArrayOfStringToOptionsTypeArray - - | Bank => - let updatedBankNames = - bankNames->DropdownField.updateArrayOfStringToOptionsTypeArray - - | SpecialField(element) => element - | InfoElement - | PixKey - | PixCPF - | PixCNPJ - | DocumentType(_) - | DocumentNumber - | CardNumber - | CardExpiryMonth - | CardExpiryYear - | CardExpiryMonthAndYear - | CardCvc - | CardExpiryAndCvc - | Currency(_) - | FullName - | GiftCardNumber - | GiftCardPin - | ShippingName // Shipping Details are currently supported by only one click widgets - | ShippingAddressLine1 - | ShippingAddressLine2 - | ShippingAddressCity - | ShippingAddressPincode - | ShippingAddressState - | ShippingAddressCountry(_) - | CryptoCurrencyNetworks - | DateOfBirth - | PhoneNumber - | PhoneCountryCode - | VpaId - | LanguagePreference(_) - | BankAccountNumber - | IBAN - | SourceBankAccountId - | None => React.null - }} -
- }) - ->React.array} -
-
-
- - - {<> - {if fieldsArr->Array.length > 1 { - bottomElement - } else { - - }} - } - - } + + }) + ->React.array} +
+
+ + + + {<> + {if processedFieldConfigs->Array.length > 1 { + bottomElement + } else { + + }} + } + + + }} + /> } diff --git a/src/Components/EmailPaymentInput.res b/src/Components/EmailPaymentInput.res index 05b4c5708..5231a4562 100644 --- a/src/Components/EmailPaymentInput.res +++ b/src/Components/EmailPaymentInput.res @@ -1,63 +1,47 @@ open RecoilAtoms open Utils -open EmailValidation @react.component -let make = () => { +let make = (~name="email") => { let {localeString} = Recoil.useRecoilValueFromAtom(configAtom) - let (email, setEmail) = Recoil.useRecoilState(userEmailAddress) let {fields} = Recoil.useRecoilValueFromAtom(optionAtom) let showDetails = PaymentType.getShowDetails(~billingDetails=fields.billingDetails) let emailRef = React.useRef(Nullable.null) + let createValidator = rule => + Validation.createFieldValidator( + rule, + ~enabledCardSchemes=[], + ~localeObject=localeString->Obj.magic, + ) + + let field: ReactFinalForm.Field.fieldProps = ReactFinalForm.useField( + name, + ~config={validate: createValidator(Validation.Email)}, + ) + + let emailValue = field.input.value->Option.getOr("") + let changeEmail = ev => { let val: string = ReactEvent.Form.target(ev)["value"] - setEmail(prev => { - value: val, - isValid: val->isEmailValid, - errorString: val->isEmailValid->Option.getOr(false) ? "" : prev.errorString, - }) - } - let onBlur = ev => { - let val = ReactEvent.Focus.target(ev)["value"] - setEmail(prev => { - ...prev, - isValid: val->isEmailValid, - }) + field.input.onChange(val) } - React.useEffect(() => { - setEmail(prev => { - ...prev, - errorString: switch prev.isValid { - | Some(val) => val ? "" : localeString.emailInvalidText - | None => "" - }, - }) - None - }, [email.isValid]) - - let submitCallback = React.useCallback((ev: Window.event) => { - let json = ev.data->safeParse - let confirm = json->getDictFromJson->ConfirmType.itemToObjMapper - if confirm.doSubmit { - if email.value == "" { - setEmail(prev => { - ...prev, - errorString: localeString.emailEmptyText, - }) - } - } - }, [email]) - useSubmitPaymentData(submitCallback) + let onBlur = (_ev: JsxEventU.Focus.t) => { + field.input.onBlur() + } ()} + value={ + RecoilAtomTypes.value: emailValue, + isValid: Some(field.meta.valid), + errorString: field.meta.touched ? field.meta.error->Option.getOr("") : "", + } onChange=changeEmail onBlur type_="email" diff --git a/src/Components/FullNamePaymentInput.res b/src/Components/FullNamePaymentInput.res index 5b539179e..91cecac9f 100644 --- a/src/Components/FullNamePaymentInput.res +++ b/src/Components/FullNamePaymentInput.res @@ -1,71 +1,90 @@ open RecoilAtoms open PaymentType -open Utils @react.component -let make = (~customFieldName=None, ~optionalRequiredFields=None) => { +let make = (~customFieldName, ~firstNamePath, ~lastNamePath) => { let {localeString} = Recoil.useRecoilValueFromAtom(configAtom) let {fields} = Recoil.useRecoilValueFromAtom(optionAtom) - let (fullName, setFullName) = Recoil.useRecoilState(userFullName) - let showDetails = getShowDetails(~billingDetails=fields.billingDetails) - let changeName = ev => { - let val: string = ReactEvent.Form.target(ev)["value"] - setFullName(prev => validateName(val, prev, localeString)) + let (placeholder, fieldName) = switch customFieldName { + | Some(val) => (val, val) + | None => (localeString.fullNamePlaceholder, localeString.fullNameLabel) } - let onBlur = ev => { - let val: string = ReactEvent.Focus.target(ev)["value"] - setFullName(prev => validateName(val, prev, localeString)) + let createValidator = rule => + Validation.createFieldValidator( + rule, + ~enabledCardSchemes=[], + ~localeObject=localeString->Obj.magic, + ) + + let showDetails = getShowDetails(~billingDetails=fields.billingDetails) + + let firstField: ReactFinalForm.Field.fieldProps = ReactFinalForm.useField( + firstNamePath, + ~config={ + validate: createValidator(Validation.FirstName), + }, + ) + let lastField: ReactFinalForm.Field.fieldProps = ReactFinalForm.useField( + lastNamePath, + ~config={ + validate: createValidator(Validation.LastName), + }, + ) + + // Local state: the combined display value shown in the single . + let (inputValue, setInputValue) = React.useState(() => "") + + let handleChange = ev => { + let value: string = ReactEvent.Form.target(ev)["value"] + setInputValue(_ => value) + let spaceIndex = value->String.indexOf(" ") + if spaceIndex === -1 { + firstField.input.onChange(value) + lastField.input.onChange("") + } else { + let firstName = value->String.substring(~start=0, ~end=spaceIndex) + let lastName = value->String.substringToEnd(~start=spaceIndex + 1) + firstField.input.onChange(firstName) + lastField.input.onChange(lastName) + } } - let (placeholder, fieldName) = switch customFieldName { - | Some(val) => (val, val) - | None => (localeString.fullNamePlaceholder, localeString.fullNameLabel) + let onBlur = (_ev: JsxEventU.Focus.t) => { + firstField.input.onBlur() + lastField.input.onBlur() } - let nameRef = React.useRef(Nullable.null) - React.useEffect(() => { - setFullName(prev => validateName(prev.value, prev, localeString)) - None - }, []) + let nameRef = React.useRef(Nullable.null) - let submitCallback = React.useCallback((ev: Window.event) => { - let json = ev.data->safeParse - let confirm = json->getDictFromJson->ConfirmType.itemToObjMapper - if confirm.doSubmit { - if fullName.value == "" { - setFullName(prev => { - ...prev, - errorString: fieldName->localeString.nameEmptyText, - }) - } else if !(fullName.isValid->Option.getOr(false)) { - setFullName(prev => { - ...prev, - errorString: localeString.invalidCardHolderNameError, - }) - } else { - switch optionalRequiredFields { - | Some(requiredFields) => - if !DynamicFieldsUtils.checkIfNameIsValid(requiredFields, FullName, fullName) { - setFullName(prev => { - ...prev, - errorString: fieldName->localeString.completeNameEmptyText, - }) - } - | None => () - } - } + let errorString = if ( + (firstField.meta.touched && !firstField.meta.active) || + (lastField.meta.touched && !lastField.meta.active) + ) { + switch (firstField.meta.error, lastField.meta.error) { + | (Some(err), _) => err + | (_, Some(err)) => err + | _ => "" } - }, [fullName]) - useSubmitPaymentData(submitCallback) + } else { + "" + } + + let isValid = + firstField.meta.valid && + (lastField.meta.valid || !lastField.meta.touched || lastField.meta.active) ()} + value={ + value: inputValue, + isValid: Some(isValid), + errorString, + } + onChange=handleChange onBlur type_="text" inputRef=nameRef diff --git a/src/Components/GiftCardNumberInput.res b/src/Components/GiftCardNumberInput.res index ac507293c..ccba68969 100644 --- a/src/Components/GiftCardNumberInput.res +++ b/src/Components/GiftCardNumberInput.res @@ -1,51 +1,43 @@ @react.component -let make = () => { +let make = (~name="giftCardNumber") => { open RecoilAtoms - open Utils let {localeString} = Recoil.useRecoilValueFromAtom(configAtom) - let (giftCardNumber, setGiftCardNumber) = Recoil.useRecoilState(userGiftCardNumber) let giftCardNumberRef = React.useRef(Nullable.null) - let updateGiftCardNumber = val => { - setGiftCardNumber(_ => { - value: val, - isValid: Some(val !== ""), - errorString: val !== "" ? "" : localeString.giftCardNumberEmptyText, - }) - } + let createValidator = rule => + Validation.createFieldValidator( + rule, + ~enabledCardSchemes=[], + ~localeObject=localeString->Obj.magic, + ) + + let field: ReactFinalForm.Field.fieldProps = ReactFinalForm.useField( + name, + ~config={validate: createValidator(Validation.GiftCardNumber)}, + ) + + let giftCardNumberValue = field.input.value->Option.getOr("") let changeGiftCardNumber = ev => { let val = ReactEvent.Form.target(ev)["value"] - updateGiftCardNumber(val) + field.input.onChange(val) } - let onBlurGiftCardNumber = ev => { - let val = ReactEvent.Focus.target(ev)["value"] - updateGiftCardNumber(val) + let onBlur = (_ev: JsxEventU.Focus.t) => { + field.input.onBlur() } - let submitCallback = React.useCallback((ev: Window.event) => { - let json = ev.data->safeParse - let confirm = json->getDictFromJson->ConfirmType.itemToObjMapper - if confirm.doSubmit { - if giftCardNumber.value == "" { - setGiftCardNumber(prev => { - ...prev, - errorString: localeString.giftCardNumberEmptyText, - }) - } - } - }, [giftCardNumber.value]) - - useSubmitPaymentData(submitCallback) - ()} + value={ + RecoilAtomTypes.value: giftCardNumberValue, + isValid: Some(field.meta.valid), + errorString: field.meta.touched ? field.meta.error->Option.getOr("") : "", + } onChange=changeGiftCardNumber - onBlur=onBlurGiftCardNumber + onBlur type_="text" name="giftCardNumber" inputRef=giftCardNumberRef diff --git a/src/Components/GiftCardPinInput.res b/src/Components/GiftCardPinInput.res index 02e7b1373..be8f0355e 100644 --- a/src/Components/GiftCardPinInput.res +++ b/src/Components/GiftCardPinInput.res @@ -1,51 +1,43 @@ @react.component -let make = () => { +let make = (~name="giftCardPin") => { open RecoilAtoms - open Utils let {localeString} = Recoil.useRecoilValueFromAtom(configAtom) - let (giftCardPin, setGiftCardPin) = Recoil.useRecoilState(userGiftCardPin) let giftCardPinRef = React.useRef(Nullable.null) - let updateGiftCardPin = val => { - setGiftCardPin(_ => { - value: val, - isValid: Some(val !== ""), - errorString: val !== "" ? "" : localeString.giftCardPinEmptyText, - }) - } + let createValidator = rule => + Validation.createFieldValidator( + rule, + ~enabledCardSchemes=[], + ~localeObject=localeString->Obj.magic, + ) + + let field: ReactFinalForm.Field.fieldProps = ReactFinalForm.useField( + name, + ~config={validate: createValidator(Validation.GiftCardPin)}, + ) + + let giftCardPinValue = field.input.value->Option.getOr("") let changeGiftCardPin = ev => { let val = ReactEvent.Form.target(ev)["value"] - updateGiftCardPin(val) + field.input.onChange(val) } - let onBlurGiftCardPin = ev => { - let val = ReactEvent.Focus.target(ev)["value"] - updateGiftCardPin(val) + let onBlur = (_ev: JsxEventU.Focus.t) => { + field.input.onBlur() } - let submitCallback = React.useCallback((ev: Window.event) => { - let json = ev.data->safeParse - let confirm = json->getDictFromJson->ConfirmType.itemToObjMapper - if confirm.doSubmit { - if giftCardPin.value == "" { - setGiftCardPin(prev => { - ...prev, - errorString: localeString.giftCardPinEmptyText, - }) - } - } - }, [giftCardPin.value]) - - useSubmitPaymentData(submitCallback) - ()} + value={ + RecoilAtomTypes.value: giftCardPinValue, + isValid: Some(field.meta.valid), + errorString: field.meta.touched ? field.meta.error->Option.getOr("") : "", + } onChange=changeGiftCardPin - onBlur=onBlurGiftCardPin + onBlur type_="text" name="giftCardPin" inputRef=giftCardPinRef diff --git a/src/Components/ManageSavedItem.res b/src/Components/ManageSavedItem.res index 7a784c0f8..f5f0ec976 100644 --- a/src/Components/ManageSavedItem.res +++ b/src/Components/ManageSavedItem.res @@ -1,4 +1,5 @@ open RecoilAtoms +open DynamicFieldsUtils @react.component let make = ( @@ -24,6 +25,49 @@ let make = ( | _ => "" } + let paymentMethodListValue = Recoil.useRecoilValueFromAtom(PaymentUtils.paymentMethodListValue) + + let paymentMethod = "card" + let paymentMethodType = "credit" + + let paymentMethodTypes = PaymentUtils.usePaymentMethodTypeFromList( + ~paymentMethodListValue, + ~paymentMethod, + ~paymentMethodType, + ) + + let (superpositionMissingFields, initialValues, _) = useSuperpositionFields( + ~paymentMethod, + ~paymentMethodType, + ~paymentMethodTypes, + ~paymentMethodListValue, + ) + + let fullNameConfig = React.useMemo(() => { + superpositionMissingFields + ->Array.find(fc => + switch fc.fieldType { + | FullNameInput(_) => true + | _ => false + } + ) + ->Option.map(fc => + switch fc.fieldType { + | FullNameInput(config) => config + | _ => {firstName: None, lastName: None} + } + ) + ->Option.getOr({firstName: None, lastName: None}) + }, [superpositionMissingFields]) + + let firstNamePath = React.useMemo(() => { + fullNameConfig.firstName->Option.map(fc => fc.outputPath)->Option.getOr("") + }, [fullNameConfig]) + + let lastNamePath = React.useMemo(() => { + fullNameConfig.lastName->Option.map(fc => fc.outputPath)->Option.getOr("") + }, [fullNameConfig]) + React.useEffect(() => { startTransition(() => { setFullName(prev => Utils.validateName(cardHolderName, prev, localeString)) @@ -32,6 +76,10 @@ let make = ( None }, []) + let formValidator = React.useMemo(() => { + _ => Dict.make() + }, []) +
- - + ()} + initialValues={Some(initialValues)} + validate={Some(formValidator)} + render={_ => { + <> + + + + }} + />
} diff --git a/src/Components/NicknamePaymentInput.res b/src/Components/NicknamePaymentInput.res index c376e7d53..dafac6190 100644 --- a/src/Components/NicknamePaymentInput.res +++ b/src/Components/NicknamePaymentInput.res @@ -1,25 +1,40 @@ @react.component -let make = () => { +let make = (~name="userCardNickName") => { open RecoilAtoms - open Utils - let (nickName, setNickName) = Recoil.useRecoilState(userCardNickName) let {localeString} = Recoil.useRecoilValueFromAtom(configAtom) + let createValidator = rule => + Validation.createFieldValidator( + rule, + ~enabledCardSchemes=[], + ~localeObject=localeString->Obj.magic, + ) + + let field: ReactFinalForm.Field.fieldProps = ReactFinalForm.useField( + name, + ~config={validate: createValidator(Validation.Nickname)}, + ) + + let nickNameValue = field.input.value->Option.getOr("") + let onChange = ev => { let val: string = ReactEvent.Form.target(ev)["value"] - setNickName(prev => setNickNameState(val, prev, localeString)) + field.input.onChange(val) } - let onBlur = ev => { - let val: string = ReactEvent.Focus.target(ev)["value"] - setNickName(prev => setNickNameState(val, prev, localeString)) + let onBlur = (_ev: JsxEventU.Focus.t) => { + field.input.onBlur() } Option.getOr("") : "", + } + setValue={_ => ()} onChange onBlur type_="userCardNickName" diff --git a/src/Components/PhoneNumberPaymentInput.res b/src/Components/PhoneNumberPaymentInput.res index 13aa49776..9cbad43ac 100644 --- a/src/Components/PhoneNumberPaymentInput.res +++ b/src/Components/PhoneNumberPaymentInput.res @@ -1,13 +1,30 @@ @react.component -let make = () => { +let make = (~numberName: string, ~codeName: string) => { open RecoilAtoms open PaymentType open Utils + let {localeString} = Recoil.useRecoilValueFromAtom(configAtom) + + let createValidator = rule => + Validation.createFieldValidator( + rule, + ~enabledCardSchemes=[], + ~localeObject=localeString->Obj.magic, + ) + + let numberField: ReactFinalForm.Field.fieldProps = ReactFinalForm.useField( + numberName, + ~config={validate: createValidator(Validation.Phone)}, + ) + let codeField: ReactFinalForm.Field.fieldProps = ReactFinalForm.useField(codeName) + + let numberVal = numberField.input.value->Option.getOr("") + let codeVal = codeField.input.value->Option.getOr("") + let phoneRef = React.useRef(Nullable.null) let {fields} = Recoil.useRecoilValueFromAtom(optionAtom) let showDetails = getShowDetails(~billingDetails=fields.billingDetails) - let (phone, setPhone) = Recoil.useRecoilState(userPhoneNumber) let clientTimeZone = CardUtils.dateTimeFormat().resolvedOptions().timeZone let clientCountry = getClientCountry(clientTimeZone) let currentCountryCode = Utils.getCountryCode(clientCountry.countryName) @@ -54,19 +71,14 @@ let make = () => { let getCountryCodeSplitValue = val => val->String.split("#")->Array.get(1)->Option.getOr("") let changePhone = ev => { - let val: string = ReactEvent.Form.target(ev)["value"]->String.replaceRegExp(%re("/\D|\s/g"), "") - setPhone(prev => { - ...prev, - countryCode: valueDropDown->getCountryCodeSplitValue, - value: val, - }) + let val: string = + ReactEvent.Form.target(ev)["value"]->String.replaceRegExp(%re("/\\D|\\s/g"), "") + numberField.input.onChange(val) + codeField.input.onChange(valueDropDown->getCountryCodeSplitValue) } React.useEffect(() => { - setPhone(prev => { - ...prev, - countryCode: valueDropDown->getCountryCodeSplitValue, - }) + codeField.input.onChange(valueDropDown->getCountryCodeSplitValue) None }, [valueDropDown]) @@ -86,7 +98,13 @@ let make = () => { Option.getOr("") : "", + } + setValue={_ => ()} onChange=changePhone paymentType=Payment type_="tel" diff --git a/src/Components/PixPaymentInput.res b/src/Components/PixPaymentInput.res index e0e5a7f26..fa1f51438 100644 --- a/src/Components/PixPaymentInput.res +++ b/src/Components/PixPaymentInput.res @@ -1,136 +1,58 @@ @react.component -let make = (~fieldType="") => { +let make = (~name: string, ~fieldType: string) => { open RecoilAtoms - open Utils let {localeString} = Recoil.useRecoilValueFromAtom(configAtom) - let (pixCNPJ, setPixCNPJ) = Recoil.useRecoilState(userPixCNPJ) - let (pixCPF, setPixCPF) = Recoil.useRecoilState(userPixCPF) - let (pixKey, setPixKey) = Recoil.useRecoilState(userPixKey) - let (sourceBankAccountId, setSourceBankAccountId) = Recoil.useRecoilState(sourceBankAccountId) let inputRef = React.useRef(Nullable.null) - let validatePixKey = (val): RecoilAtomTypes.field => - if val->String.length > 0 { - {value: val, isValid: Some(true), errorString: ""} - } else { - {value: val, isValid: None, errorString: ""} - } - - let validatePixCNPJ = (val): RecoilAtomTypes.field => { - let isCNPJValid = %re("/^\d*$/")->RegExp.test(val) && val->String.length === 14 - if isCNPJValid { - {value: val, isValid: Some(true), errorString: ""} - } else if val->String.length === 0 { - {value: val, isValid: None, errorString: ""} - } else { - { - value: val, - isValid: Some(false), - errorString: localeString.pixCNPJInvalidText, - } - } - } - - let validatePixCPF = (val): RecoilAtomTypes.field => { - let isCPFValid = %re("/^\d*$/")->RegExp.test(val) && val->String.length === 11 - if isCPFValid { - {value: val, isValid: Some(true), errorString: ""} - } else if val->String.length === 0 { - {value: val, isValid: None, errorString: ""} - } else { - { - value: val, - isValid: Some(false), - errorString: localeString.pixCPFInvalidText, - } - } - } - - let (fieldName, setValue, value, placeholder, maxLength, validationFn) = switch fieldType { - | "pixKey" => ( - localeString.pixKeyLabel, - setPixKey, - pixKey, - localeString.pixKeyPlaceholder, - None, - validatePixKey, + let createValidator = rule => + Validation.createFieldValidator( + rule, + ~enabledCardSchemes=[], + ~localeObject=localeString->Obj.magic, ) + + let (fieldName, placeholder, maxLength, validationRule) = switch fieldType { + | "pixKey" => (localeString.pixKeyLabel, localeString.pixKeyPlaceholder, None, Validation.PixKey) | "pixCPF" => ( localeString.pixCPFLabel, - setPixCPF, - pixCPF, localeString.pixCPFPlaceholder, Some(11), - validatePixCPF, + Validation.PixCPF, ) | "pixCNPJ" => ( localeString.pixCNPJLabel, - setPixCNPJ, - pixCNPJ, localeString.pixCNPJPlaceholder, Some(14), - validatePixCNPJ, - ) - | _ => ( - "", - _ => (), - RecoilAtoms.defaultFieldValues, - "", - None, - _ => RecoilAtoms.defaultFieldValues, + Validation.PixCNPJ, ) + | _ => ("", "", None, Validation.Required) } - let validateAndSetPixInputValue = val => setValue(_ => val->validationFn) + let field: ReactFinalForm.Field.fieldProps = ReactFinalForm.useField( + name, + ~config={validate: createValidator(validationRule)}, + ) + + let pixValue = field.input.value->Option.getOr("") let onChange = ev => { let val = ReactEvent.Form.target(ev)["value"] - validateAndSetPixInputValue(val) + field.input.onChange(val) } - let onBlur = ev => { - let val = ReactEvent.Focus.target(ev)["value"] - validateAndSetPixInputValue(val) + let onBlur = (_ev: JsxEventU.Focus.t) => { + field.input.onBlur() } - let submitCallback = React.useCallback((ev: Window.event) => { - let json = ev.data->safeParse - let confirm = json->getDictFromJson->ConfirmType.itemToObjMapper - if confirm.doSubmit { - if pixKey.value == "" { - setPixKey(prev => { - ...prev, - errorString: localeString.pixKeyEmptyText, - }) - } - if pixCNPJ.value == "" { - setPixCNPJ(prev => { - ...prev, - errorString: localeString.pixCNPJEmptyText, - }) - } - if pixCPF.value == "" { - setPixCPF(prev => { - ...prev, - errorString: localeString.pixCPFEmptyText, - }) - } - if sourceBankAccountId.value == "" { - setSourceBankAccountId(prev => { - ...prev, - errorString: localeString.sourceBankAccountIdEmptyText, - }) - } - } - }, [pixCNPJ.value, pixKey.value, pixCPF.value]) - - useSubmitPaymentData(submitCallback) - ()} + value={ + RecoilAtomTypes.value: pixValue, + isValid: Some(field.meta.valid), + errorString: field.meta.touched ? field.meta.error->Option.getOr("") : "", + } onChange onBlur type_=fieldType diff --git a/src/Components/ReactFinalFormField.res b/src/Components/ReactFinalFormField.res new file mode 100644 index 000000000..f72ecd033 --- /dev/null +++ b/src/Components/ReactFinalFormField.res @@ -0,0 +1,18 @@ +@react.component +let make = (~name: string, ~validationRule=?, ~render) => { + let {localeString} = Recoil.useRecoilValueFromAtom(RecoilAtoms.configAtom) + + let createValidator = rule => + Validation.createFieldValidator( + rule, + ~enabledCardSchemes=[], + ~localeObject=localeString->Obj.magic, + ) + + let field: ReactFinalForm.Field.fieldProps = switch validationRule { + | Some(rule) => ReactFinalForm.useField(name, ~config={validate: createValidator(rule)}) + | None => ReactFinalForm.useField(name) + } + + render(field) +} diff --git a/src/Components/VpaIdPaymentInput.res b/src/Components/VpaIdPaymentInput.res index c3fe5df03..a6a13215e 100644 --- a/src/Components/VpaIdPaymentInput.res +++ b/src/Components/VpaIdPaymentInput.res @@ -2,53 +2,41 @@ open RecoilAtoms open Utils @react.component -let make = () => { +let make = (~name: string) => { let {localeString} = Recoil.useRecoilValueFromAtom(configAtom) - let (vpaId, setVpaId) = Recoil.useRecoilState(userVpaId) + let createValidator = rule => + Validation.createFieldValidator( + rule, + ~enabledCardSchemes=[], + ~localeObject=localeString->Obj.magic, + ) + + let field: ReactFinalForm.Field.fieldProps = ReactFinalForm.useField( + name, + ~config={validate: createValidator(Validation.VpaId)}, + ) + + let vpaIdValue = field.input.value->Option.getOr("") let vpaIdRef = React.useRef(Nullable.null) let changeVpaId = ev => { let val: string = ReactEvent.Form.target(ev)["value"] - setVpaId(prev => { - value: val, - isValid: val->isVpaIdValid, - errorString: val->isVpaIdValid->Option.getOr(false) ? "" : prev.errorString, - }) + field.input.onChange(val) } - let onBlur = ev => { - let val = ReactEvent.Focus.target(ev)["value"] - let isValid = val->isVpaIdValid - let errorString = switch isValid { - | Some(val) => val ? "" : localeString.vpaIdInvalidText - | None => "" - } - setVpaId(prev => { - ...prev, - isValid, - errorString, - }) + let onBlur = (_ev: JsxEventU.Focus.t) => { + field.input.onBlur() } - let submitCallback = React.useCallback((ev: Window.event) => { - let json = ev.data->safeParse - let confirm = json->getDictFromJson->ConfirmType.itemToObjMapper - if confirm.doSubmit { - if vpaId.value == "" { - setVpaId(prev => { - ...prev, - errorString: localeString.vpaIdEmptyText, - }) - } - } - }, [vpaId]) - useSubmitPaymentData(submitCallback) - ()} + value={ + RecoilAtomTypes.value: vpaIdValue, + isValid: Some(field.meta.valid), + errorString: field.meta.touched ? field.meta.error->Option.getOr("") : "", + } onChange=changeVpaId onBlur type_="text" diff --git a/src/LocaleStrings/ArabicLocale.res b/src/LocaleStrings/ArabicLocale.res index 0287315df..78a7e6f16 100644 --- a/src/LocaleStrings/ArabicLocale.res +++ b/src/LocaleStrings/ArabicLocale.res @@ -79,6 +79,7 @@ let localeStrings: LocaleStringTypes.localeStrings = { billingNameLabel: `اسم الفواتير`, billingNamePlaceholder: `الاسم الأول والاسم الأخير`, cardHolderName: `إسم صاحب البطاقة`, + cardHolderNameRequiredText: `اسم حامل البطاقة مطلوب`, on: `على`, \"and": `و`, nameEmptyText: str => `يرجى تقديم الخاص بك ${str}`, @@ -251,4 +252,5 @@ let localeStrings: LocaleStringTypes.localeStrings = { giftCardPaymentCompleteMessage: `لا يوجد مبلغ متبقٍ للدفع. يرجى المتابعة لإتمام عملية الدفع.`, giftCardPaymentRemainingMessage: (currency, amount) => `يرجى دفع المبلغ المتبقي ${amount} ${currency} باستخدام وسيلة دفع أخرى أدناه.`, + mandatoryFieldText: "هذا الحقل إلزامي.", } diff --git a/src/LocaleStrings/CatalanLocale.res b/src/LocaleStrings/CatalanLocale.res index 92c940470..2dc01b01a 100644 --- a/src/LocaleStrings/CatalanLocale.res +++ b/src/LocaleStrings/CatalanLocale.res @@ -52,6 +52,7 @@ let localeStrings: LocaleStringTypes.localeStrings = { card: `Targeta`, billingNameLabel: `Nom de facturació`, cardHolderName: `Nom del titular de la targeta`, + cardHolderNameRequiredText: `Cal el nom del titular de la targeta`, cardNickname: `Sobrenom de la targeta`, billingNamePlaceholder: `Nom i cognom`, ibanEmptyText: `L'IBAN no pot estar buit`, @@ -250,4 +251,5 @@ let localeStrings: LocaleStringTypes.localeStrings = { giftCardPaymentCompleteMessage: `No queda cap import pendent de pagament. Si us plau, continueu amb el pagament.`, giftCardPaymentRemainingMessage: (currency, amount) => `Pagueu l'import restant de ${currency} ${amount} amb un altre mètode de pagament a continuació.`, + mandatoryFieldText: "Aquest camp és obligatori", } diff --git a/src/LocaleStrings/ChineseLocale.res b/src/LocaleStrings/ChineseLocale.res index acb44fb1e..8eb43e4b9 100644 --- a/src/LocaleStrings/ChineseLocale.res +++ b/src/LocaleStrings/ChineseLocale.res @@ -79,6 +79,7 @@ let localeStrings: LocaleStringTypes.localeStrings = { billingNameLabel: `适用额外费用`, billingNamePlaceholder: `名字和姓氏`, cardHolderName: `持卡人姓名`, + cardHolderNameRequiredText: `持卡人姓名必填`, on: `在`, \"and": `和`, nameEmptyText: str => `请提供您的 ${str}`, @@ -248,4 +249,5 @@ let localeStrings: LocaleStringTypes.localeStrings = { giftCardPaymentCompleteMessage: `无需支付剩余金额。请继续完成付款。`, giftCardPaymentRemainingMessage: (currency, amount) => `请使用下方的其他支付方式支付剩余金额 ${currency}${amount}。`, + mandatoryFieldText: "此字段为必填项", } diff --git a/src/LocaleStrings/DeutschLocale.res b/src/LocaleStrings/DeutschLocale.res index d743842b0..296b19948 100644 --- a/src/LocaleStrings/DeutschLocale.res +++ b/src/LocaleStrings/DeutschLocale.res @@ -79,6 +79,7 @@ let localeStrings: LocaleStringTypes.localeStrings = { billingNameLabel: `Abrechnungsname`, billingNamePlaceholder: `Vor-und Nachname`, cardHolderName: `Name des Karteninhabers`, + cardHolderNameRequiredText: `Name des Karteninhabers erforderlich`, on: `An`, \"and": `Und`, nameEmptyText: str => `Bitte geben Sie Ihre an ${str}`, @@ -249,4 +250,5 @@ let localeStrings: LocaleStringTypes.localeStrings = { giftCardPaymentCompleteMessage: `Es ist kein Restbetrag zu zahlen. Bitte fahren Sie mit der Zahlung fort.`, giftCardPaymentRemainingMessage: (currency, amount) => `Bitte zahlen Sie den verbleibenden Betrag von ${amount} ${currency} mit einer anderen Zahlungsmethode unten.`, + mandatoryFieldText: "Dieses Feld ist obligatorisch", } diff --git a/src/LocaleStrings/DutchLocale.res b/src/LocaleStrings/DutchLocale.res index cacc431f7..3ec0e6b11 100644 --- a/src/LocaleStrings/DutchLocale.res +++ b/src/LocaleStrings/DutchLocale.res @@ -52,6 +52,7 @@ let localeStrings: LocaleStringTypes.localeStrings = { card: `Kort`, billingNameLabel: `Faktureringsnavn`, cardHolderName: `Naam van de kaarthouder`, + cardHolderNameRequiredText: `Naam van de kaarthouder vereist`, cardNickname: `Kaartbijnaam`, billingNamePlaceholder: `Voornaam en achternaam`, ibanEmptyText: `IBAN mag niet leeg zijn`, @@ -248,4 +249,5 @@ let localeStrings: LocaleStringTypes.localeStrings = { giftCardPaymentCompleteMessage: `Er staat geen resterend bedrag open. Ga verder met de betaling.`, giftCardPaymentRemainingMessage: (currency, amount) => `Betaal het resterende bedrag van ${amount} ${currency} met een andere betaalmethode hieronder.`, + mandatoryFieldText: "This field is mandatory", } diff --git a/src/LocaleStrings/EnglishGBLocale.res b/src/LocaleStrings/EnglishGBLocale.res index b2547c586..f871a44f5 100644 --- a/src/LocaleStrings/EnglishGBLocale.res +++ b/src/LocaleStrings/EnglishGBLocale.res @@ -79,6 +79,7 @@ let localeStrings: LocaleStringTypes.localeStrings = { billingNameLabel: "Billing name", billingNamePlaceholder: "First and last name", cardHolderName: "Card Holder Name", + cardHolderNameRequiredText: "Card Holder's name required", on: "on", \"and": "and", nameEmptyText: str => `Please provide your ${str}`, @@ -248,4 +249,5 @@ let localeStrings: LocaleStringTypes.localeStrings = { giftCardPaymentCompleteMessage: `There is no remaining amount to pay. Please proceed with the payment.`, giftCardPaymentRemainingMessage: (currency, amount) => `Please pay the remaining ${currency} ${amount} using another payment method below.`, + mandatoryFieldText: "This field is required", } diff --git a/src/LocaleStrings/EnglishLocale.res b/src/LocaleStrings/EnglishLocale.res index e06cfc24a..4dbe29d17 100644 --- a/src/LocaleStrings/EnglishLocale.res +++ b/src/LocaleStrings/EnglishLocale.res @@ -79,6 +79,7 @@ let localeStrings: LocaleStringTypes.localeStrings = { billingNameLabel: "Billing name", billingNamePlaceholder: "First and last name", cardHolderName: "Card Holder Name", + cardHolderNameRequiredText: "Card Holder's name required", on: "on", \"and": "and", nameEmptyText: str => `Please provide your ${str}`, @@ -248,4 +249,5 @@ let localeStrings: LocaleStringTypes.localeStrings = { giftCardPaymentCompleteMessage: `No remaining amount to pay. Please proceed with payment.`, giftCardPaymentRemainingMessage: (currency, amount) => `Pay remaining ${currency} ${amount} with other payment method below.`, + mandatoryFieldText: "This field is mandatory", } diff --git a/src/LocaleStrings/FrenchBelgiumLocale.res b/src/LocaleStrings/FrenchBelgiumLocale.res index 940fd33c1..c571681c7 100644 --- a/src/LocaleStrings/FrenchBelgiumLocale.res +++ b/src/LocaleStrings/FrenchBelgiumLocale.res @@ -52,6 +52,7 @@ let localeStrings: LocaleStringTypes.localeStrings = { card: `Carte`, billingNameLabel: `Nom de facturation`, cardHolderName: `Nom du titulaire`, + cardHolderNameRequiredText: `Nom du titulaire de la carte requis`, cardNickname: `Pseudonyme de la carte`, billingNamePlaceholder: `Nom et prénom`, ibanEmptyText: `L'IBAN ne peut pas être vide`, @@ -250,4 +251,5 @@ let localeStrings: LocaleStringTypes.localeStrings = { giftCardPaymentCompleteMessage: `Aucun montant restant à payer. Veuillez poursuivre le paiement.`, giftCardPaymentRemainingMessage: (currency, amount) => `Veuillez payer le montant restant de ${amount} ${currency} avec un autre moyen de paiement ci-dessous.`, + mandatoryFieldText: "Ce champ est obligatoire", } diff --git a/src/LocaleStrings/FrenchLocale.res b/src/LocaleStrings/FrenchLocale.res index d504b65ed..0f9d96ac2 100644 --- a/src/LocaleStrings/FrenchLocale.res +++ b/src/LocaleStrings/FrenchLocale.res @@ -79,6 +79,7 @@ let localeStrings: LocaleStringTypes.localeStrings = { billingNameLabel: `Nom de facturation`, billingNamePlaceholder: `Prénom et nom de famille`, cardHolderName: `Nom du titulaire`, + cardHolderNameRequiredText: `Nom du titulaire de la carte requis`, on: `sur`, \"and": `et`, nameEmptyText: str => `Veuillez fournir votre ${str}`, @@ -250,4 +251,5 @@ let localeStrings: LocaleStringTypes.localeStrings = { giftCardPaymentCompleteMessage: `Aucun montant restant à payer. Veuillez poursuivre le paiement.`, giftCardPaymentRemainingMessage: (currency, amount) => `Veuillez payer le montant restant de ${amount} ${currency} avec un autre moyen de paiement ci-dessous.`, + mandatoryFieldText: "Ce champ est obligatoire", } diff --git a/src/LocaleStrings/HebrewLocale.res b/src/LocaleStrings/HebrewLocale.res index d25097c3b..2a8dd1a67 100644 --- a/src/LocaleStrings/HebrewLocale.res +++ b/src/LocaleStrings/HebrewLocale.res @@ -79,6 +79,7 @@ let localeStrings: LocaleStringTypes.localeStrings = { billingNameLabel: `שם החיוב`, billingNamePlaceholder: `שם פרטי ושם משפחה`, cardHolderName: `שם בעל הכרטיס`, + cardHolderNameRequiredText: `נדرش שם בעל הכרטיס`, on: `עַל`, \"and": `ו`, nameEmptyText: str => `אנא ספק את שלך ${str}`, @@ -249,4 +250,5 @@ let localeStrings: LocaleStringTypes.localeStrings = { giftCardPaymentCompleteMessage: `לא נותר סכום לתשלום. אנא המשיכו לביצוע התשלום.`, giftCardPaymentRemainingMessage: (currency, amount) => `אנא שלמו את הסכום שנותר ${amount} ${currency} באמצעות אמצעי תשלום אחר למטה.`, + mandatoryFieldText: "שדה זה הוא חובה", } diff --git a/src/LocaleStrings/ItalianLocale.res b/src/LocaleStrings/ItalianLocale.res index af45633fd..cd71a5c6d 100644 --- a/src/LocaleStrings/ItalianLocale.res +++ b/src/LocaleStrings/ItalianLocale.res @@ -52,6 +52,7 @@ let localeStrings: LocaleStringTypes.localeStrings = { card: `Carta`, billingNameLabel: `Intestatario della fattura`, cardHolderName: `Nome del titolare della carta`, + cardHolderNameRequiredText: `Il nome del titolare della carta è obbligatorio`, cardNickname: `Soprannome della carta`, billingNamePlaceholder: `Nome e cognome`, ibanEmptyText: `L'IBAN non può essere vuoto`, @@ -250,4 +251,5 @@ let localeStrings: LocaleStringTypes.localeStrings = { giftCardPaymentCompleteMessage: `Non rimane alcun importo da pagare. Procedi con il pagamento.`, giftCardPaymentRemainingMessage: (currency, amount) => `Paga l’importo rimanente di ${amount} ${currency} con un altro metodo di pagamento qui sotto.`, + mandatoryFieldText: "Questo campo è obbligatorio", } diff --git a/src/LocaleStrings/JapaneseLocale.res b/src/LocaleStrings/JapaneseLocale.res index ad92ac3a4..fc7ea4275 100644 --- a/src/LocaleStrings/JapaneseLocale.res +++ b/src/LocaleStrings/JapaneseLocale.res @@ -79,6 +79,7 @@ let localeStrings: LocaleStringTypes.localeStrings = { billingNameLabel: `課金名`, billingNamePlaceholder: `名前と苗字`, cardHolderName: `クレジットカード名義人氏名`, + cardHolderNameRequiredText: `カード所有者の名前が必要です`, on: `の上`, \"and": `そして`, nameEmptyText: str => `あなたの情報を提供してください ${str}`, @@ -249,4 +250,5 @@ let localeStrings: LocaleStringTypes.localeStrings = { giftCardPaymentCompleteMessage: `残りの支払金額はありません。支払いを続行してください。`, giftCardPaymentRemainingMessage: (currency, amount) => `残りの ${amount}${currency} は、以下の別の支払い方法でお支払いください。`, + mandatoryFieldText: "このフィールドは必須です", } diff --git a/src/LocaleStrings/LocaleStringTypes.res b/src/LocaleStrings/LocaleStringTypes.res index ba2c9f689..3f1549d11 100644 --- a/src/LocaleStrings/LocaleStringTypes.res +++ b/src/LocaleStrings/LocaleStringTypes.res @@ -68,6 +68,7 @@ type localeStrings = { billingNameLabel: string, billingNamePlaceholder: string, cardHolderName: string, + cardHolderNameRequiredText: string, on: string, \"and": string, nameEmptyText: string => string, @@ -236,6 +237,7 @@ type localeStrings = { giftCardAppliedText: string, giftCardPaymentCompleteMessage: string, giftCardPaymentRemainingMessage: (string, string) => string, + mandatoryFieldText: string, } type constantStrings = { diff --git a/src/LocaleStrings/PolishLocale.res b/src/LocaleStrings/PolishLocale.res index 636eb59a9..c1ae58dd8 100644 --- a/src/LocaleStrings/PolishLocale.res +++ b/src/LocaleStrings/PolishLocale.res @@ -52,6 +52,7 @@ let localeStrings: LocaleStringTypes.localeStrings = { card: `Karta`, billingNameLabel: `Nazwisko do faktury`, cardHolderName: `Imię i nazwisko posiadacza karty`, + cardHolderNameRequiredText: `wymagane nazwisko właściciela karty`, cardNickname: `Przezwisko karty`, billingNamePlaceholder: `Imię i nazwisko`, ibanEmptyText: `IBAN nie może być pusty`, @@ -249,4 +250,5 @@ let localeStrings: LocaleStringTypes.localeStrings = { giftCardPaymentCompleteMessage: `Nie pozostała żadna kwota do zapłaty. Proszę kontynuować płatność.`, giftCardPaymentRemainingMessage: (currency, amount) => `Proszę zapłacić pozostałą kwotę ${amount} ${currency} inną metodą płatności poniżej.`, + mandatoryFieldText: "To pole jest obowiązkowe", } diff --git a/src/LocaleStrings/PortugueseLocale.res b/src/LocaleStrings/PortugueseLocale.res index c1c19c867..ab56e99f6 100644 --- a/src/LocaleStrings/PortugueseLocale.res +++ b/src/LocaleStrings/PortugueseLocale.res @@ -52,6 +52,7 @@ let localeStrings: LocaleStringTypes.localeStrings = { card: `Cartão`, billingNameLabel: `Nome de faturação`, cardHolderName: `Nome do titular do cartão`, + cardHolderNameRequiredText: `Nome do titular do cartão obrigatório`, cardNickname: `Apelido do cartão`, billingNamePlaceholder: `Nome e sobrenome`, ibanEmptyText: `O IBAN não pode estar vazio`, @@ -249,4 +250,5 @@ let localeStrings: LocaleStringTypes.localeStrings = { giftCardPaymentCompleteMessage: `Não há valor restante a pagar. Por favor, prossiga com o pagamento.`, giftCardPaymentRemainingMessage: (currency, amount) => `Por favor, pague o valor restante de ${amount} ${currency} com outro método de pagamento abaixo.`, + mandatoryFieldText: "Este campo é obrigatório", } diff --git a/src/LocaleStrings/RussianLocale.res b/src/LocaleStrings/RussianLocale.res index 9d1dc172e..66e4c261b 100644 --- a/src/LocaleStrings/RussianLocale.res +++ b/src/LocaleStrings/RussianLocale.res @@ -52,6 +52,7 @@ let localeStrings: LocaleStringTypes.localeStrings = { card: `Карта`, billingNameLabel: `Имя плательщика`, cardHolderName: `Имя держателя карты`, + cardHolderNameRequiredText: `Требуется имя держателя карты`, cardNickname: `Прозвище карты`, billingNamePlaceholder: `Имя и фамилия`, ibanEmptyText: `IBAN не может быть пустым`, @@ -257,4 +258,5 @@ let localeStrings: LocaleStringTypes.localeStrings = { giftCardPaymentCompleteMessage: `Оставшаяся сумма к оплате отсутствует. Пожалуйста, продолжите оплату.`, giftCardPaymentRemainingMessage: (currency, amount) => `Пожалуйста, оплатите оставшуюся сумму ${amount} ${currency} другим способом оплаты ниже.`, + mandatoryFieldText: "Это поле обязательно для заполнения.", } diff --git a/src/LocaleStrings/SpanishLocale.res b/src/LocaleStrings/SpanishLocale.res index 968892542..c0761059b 100644 --- a/src/LocaleStrings/SpanishLocale.res +++ b/src/LocaleStrings/SpanishLocale.res @@ -52,6 +52,7 @@ let localeStrings: LocaleStringTypes.localeStrings = { card: `Tarjeta`, billingNameLabel: `Nombre de facturación`, cardHolderName: `Nombre del titular de la tarjeta`, + cardHolderNameRequiredText: `Se requiere el nombre del titular de la tarjeta`, cardNickname: `Apodo de la tarjeta`, billingNamePlaceholder: `Nombre y apellido`, ibanEmptyText: `El IBAN no puede estar vacío`, @@ -249,4 +250,5 @@ let localeStrings: LocaleStringTypes.localeStrings = { giftCardPaymentCompleteMessage: `No queda importe restante por pagar. Por favor, proceda con el pago.`, giftCardPaymentRemainingMessage: (currency, amount) => `Pague los ${currency} ${amount} restantes con otro método de pago a continuación.`, + mandatoryFieldText: "Este campo es obligatorio", } diff --git a/src/LocaleStrings/SwedishLocale.res b/src/LocaleStrings/SwedishLocale.res index 178af1950..60413d30c 100644 --- a/src/LocaleStrings/SwedishLocale.res +++ b/src/LocaleStrings/SwedishLocale.res @@ -52,6 +52,7 @@ let localeStrings: LocaleStringTypes.localeStrings = { card: `Kort`, billingNameLabel: `Faktureringsnamn`, cardHolderName: `Korthållarens namn`, + cardHolderNameRequiredText: `Korthållarens namn krävs`, cardNickname: `Kortets smeknamn`, billingNamePlaceholder: `Förnamn och efternamn`, ibanEmptyText: `IBAN får inte vara tomt`, @@ -248,4 +249,5 @@ let localeStrings: LocaleStringTypes.localeStrings = { giftCardPaymentCompleteMessage: `Ingen kvarvarande summa att betala. Vänligen fortsätt med betalningen.`, giftCardPaymentRemainingMessage: (currency, amount) => `Vänligen betala den kvarvarande summan ${amount} ${currency} med en annan betalningsmetod nedan.`, + mandatoryFieldText: "Detta fält är obligatoriskt", } diff --git a/src/LocaleStrings/TraditionalChineseLocale.res b/src/LocaleStrings/TraditionalChineseLocale.res index 358ab60f5..c63c7c87d 100644 --- a/src/LocaleStrings/TraditionalChineseLocale.res +++ b/src/LocaleStrings/TraditionalChineseLocale.res @@ -79,6 +79,7 @@ let localeStrings: LocaleStringTypes.localeStrings = { billingNameLabel: "帳單姓名", billingNamePlaceholder: "名字和姓氏", cardHolderName: "持卡人姓名", + cardHolderNameRequiredText: "持卡人姓名必填", on: "於", \"and": "及", nameEmptyText: str => `請提供您的${str}`, @@ -248,4 +249,5 @@ let localeStrings: LocaleStringTypes.localeStrings = { giftCardPaymentCompleteMessage: `無需支付剩餘金額。請繼續完成付款。`, giftCardPaymentRemainingMessage: (currency, amount) => `請使用下方的其他付款方式支付剩餘金額 ${currency}${amount}。`, + mandatoryFieldText: "此欄位為必填項", } diff --git a/src/Payments/ACHBankDebit.res b/src/Payments/ACHBankDebit.res index bc14d311d..84279ad46 100644 --- a/src/Payments/ACHBankDebit.res +++ b/src/Payments/ACHBankDebit.res @@ -1,18 +1,17 @@ open RecoilAtoms open Utils -open PaymentModeType +open ACHTypes @react.component let make = () => { let {themeObj} = Recoil.useRecoilValueFromAtom(configAtom) let {displaySavedPaymentMethods} = Recoil.useRecoilValueFromAtom(optionAtom) let isManualRetryEnabled = Recoil.useRecoilValueFromAtom(isManualRetryEnabled) + let areRequiredFieldsValid = Recoil.useRecoilValueFromAtom(areRequiredFieldsValid) + let areRequiredFieldsEmpty = Recoil.useRecoilValueFromAtom(areRequiredFieldsEmpty) let loggerState = Recoil.useRecoilValueFromAtom(loggerAtom) - let email = Recoil.useRecoilValueFromAtom(userEmailAddress) - let fullName = Recoil.useRecoilValueFromAtom(userFullName) - let intent = PaymentHelpers.usePaymentIntent(Some(loggerState), BankDebits) let (bankError, setBankError) = React.useState(_ => "") @@ -22,14 +21,7 @@ let make = () => { let (modalData, setModalData) = React.useState(_ => None) let toolTipRef = React.useRef(Nullable.null) - let line1 = Recoil.useRecoilValueFromAtom(userAddressline1) - let line2 = Recoil.useRecoilValueFromAtom(userAddressline2) - let country = Recoil.useRecoilValueFromAtom(userAddressCountry) - let city = Recoil.useRecoilValueFromAtom(userAddressCity) - let postalCode = Recoil.useRecoilValueFromAtom(userAddressPincode) - let state = Recoil.useRecoilValueFromAtom(userAddressState) - let countryCode = Utils.getCountryCode(country.value).isoAlpha2 - let stateCode = Utils.getStateCodeFromStateName(state.value, countryCode) + let paymentMethodListValue = Recoil.useRecoilValueFromAtom(PaymentUtils.paymentMethodListValue) let pmAuthMapper = React.useMemo1( @@ -56,12 +48,10 @@ let make = () => { None }, [modalData]) - let complete = - email.value != "" && - fullName.value != "" && - email.isValid->Option.getOr(false) && - modalData->Option.isSome - let empty = email.value == "" || fullName.value != "" + let (requiredFieldsBody, setRequiredFieldsBody) = React.useState(_ => Dict.make()) + + let complete = areRequiredFieldsValid && !areRequiredFieldsEmpty && modalData->Option.isSome + let empty = areRequiredFieldsEmpty UtilityHooks.useHandlePostMessages(~complete, ~empty, ~paymentType="ach_bank_debit") @@ -75,18 +65,31 @@ let make = () => { } if complete { switch modalData { - | Some(data) => - let body = PaymentBody.achBankDebitBody( - ~email=email.value, - ~bank=data, - ~cardHolderName=fullName.value, - ~line1=line1.value, - ~line2=line2.value, - ~country=countryCode, - ~city=city.value, - ~postalCode=postalCode.value, - ~stateCode, - ) + | Some(data: ACHTypes.data) => + let body = + PaymentBody.dynamicPaymentBody("bank_debit", "ach") + ->getJsonFromArrayOfJson + ->flattenObject(true) + ->mergeTwoFlattenedJsonDicts(requiredFieldsBody) + ->getArrayOfTupleFromDict + ->Array.concat([ + ( + "payment_method_data.bank_debit.ach_bank_debit.account_number", + data.accountNumber->JSON.Encode.string, + ), + ( + "payment_method_data.bank_debit.ach_bank_debit.routing_number", + data.routingNumber->JSON.Encode.string, + ), + ( + "payment_method_data.bank_debit.ach_bank_debit.bank_account_holder_name", + data.accountHolderName->JSON.Encode.string, + ), + ( + "payment_method_data.bank_debit.ach_bank_debit.account_type", + data.accountType->JSON.Encode.string, + ), + ]) intent( ~bodyArr=body, ~confirmParam=confirm.confirmParams, @@ -100,7 +103,13 @@ let make = () => { postFailedSubmitResponse(~errortype="validation_error", ~message="Please enter all fields") } } - }, (email, modalData, fullName, isManualRetryEnabled)) + }, ( + modalData, + isManualRetryEnabled, + requiredFieldsBody, + areRequiredFieldsValid, + areRequiredFieldsEmpty, + )) useSubmitPaymentData(submitCallback) let paymentMethodType = "ach" @@ -112,8 +121,7 @@ let make = () => {
- - +
String.length > 0}> diff --git a/src/Payments/BacsBankDebit.res b/src/Payments/BacsBankDebit.res index 1d97cb76d..3ef3dedf9 100644 --- a/src/Payments/BacsBankDebit.res +++ b/src/Payments/BacsBankDebit.res @@ -1,5 +1,4 @@ open RecoilAtoms -open RecoilAtomTypes open Utils let formatSortCode = sortcode => { @@ -28,20 +27,12 @@ let make = () => { let {displaySavedPaymentMethods} = Recoil.useRecoilValueFromAtom(optionAtom) let intent = PaymentHelpers.usePaymentIntent(Some(loggerState), BankDebits) - let email = Recoil.useRecoilValueFromAtom(userEmailAddress) - let line1 = Recoil.useRecoilValueFromAtom(userAddressline1) - let line2 = Recoil.useRecoilValueFromAtom(userAddressline2) - let country = Recoil.useRecoilValueFromAtom(userAddressCountry) - let city = Recoil.useRecoilValueFromAtom(userAddressCity) - let postalCode = Recoil.useRecoilValueFromAtom(userAddressPincode) - let state = Recoil.useRecoilValueFromAtom(userAddressState) - let fullName = Recoil.useRecoilValueFromAtom(userFullName) let setComplete = Recoil.useSetRecoilState(fieldsComplete) let (sortcode, setSortcode) = React.useState(_ => "") let (accountNumber, setAccountNumber) = React.useState(_ => "") let paymentMethodListValue = Recoil.useRecoilValueFromAtom(PaymentUtils.paymentMethodListValue) - let countryCode = Utils.getCountryCode(country.value).isoAlpha2 - let stateCode = Utils.getStateCodeFromStateName(state.value, countryCode) + let areRequiredFieldsValid = Recoil.useRecoilValueFromAtom(areRequiredFieldsValid) + let areRequiredFieldsEmpty = Recoil.useRecoilValueFromAtom(areRequiredFieldsEmpty) let (sortCodeError, setSortCodeError) = React.useState(_ => "") @@ -57,25 +48,15 @@ let make = () => { let isVerifyPMAuthConnectorConfigured = displaySavedPaymentMethods && pmAuthMapper->Dict.get("sepa")->Option.isSome + let (requiredFieldsBody, setRequiredFieldsBody) = React.useState(_ => Dict.make()) + let complete = - email.value != "" && - email.isValid->Option.getOr(false) && + areRequiredFieldsValid && + !areRequiredFieldsEmpty && sortcode->cleanSortCode->String.length == 6 && - accountNumber != "" && - fullName.value != "" && - isAddressComplete(line1, state, city, country, postalCode) && - postalCode.isValid->Option.getOr(false) - - let empty = - email.value == "" || - sortcode == "" || - fullName.value != "" || - accountNumber == "" || - line1.value == "" && line2.value == "" || - city.value == "" || - postalCode.value == "" || - country.value == "" || - state.value == "" + accountNumber != "" + + let empty = areRequiredFieldsEmpty || sortcode == "" || accountNumber == "" UtilityHooks.useHandlePostMessages(~complete, ~empty, ~paymentType="bacs_bank_debit") @@ -90,18 +71,22 @@ let make = () => { if confirm.doSubmit { if complete { - let body = PaymentBody.bacsBankDebitBody( - ~email=email.value, - ~accNum=accountNumber, - ~sortCode=sortcode, - ~line1=line1.value, - ~line2=line2.value, - ~city=city.value, - ~zip=postalCode.value, - ~stateCode, - ~country=countryCode, - ~bankAccountHolderName=fullName.value, - ) + let body = + PaymentBody.dynamicPaymentBody("bank_debit", "bacs") + ->getJsonFromArrayOfJson + ->flattenObject(true) + ->mergeTwoFlattenedJsonDicts(requiredFieldsBody) + ->getArrayOfTupleFromDict + ->Array.concat([ + ( + "payment_method_data.bank_debit.bacs_bank_debit.account_number", + accountNumber->JSON.Encode.string, + ), + ( + "payment_method_data.bank_debit.bacs_bank_debit.sort_code", + sortcode->cleanSortCode->JSON.Encode.string, + ), + ]) intent( ~bodyArr=body, ~confirmParam=confirm.confirmParams, @@ -163,9 +148,7 @@ let make = () => { placeholder="00012345" />
- - - +
diff --git a/src/Payments/BankDebitModal.res b/src/Payments/BankDebitModal.res index 119f19034..7db38e49c 100644 --- a/src/Payments/BankDebitModal.res +++ b/src/Payments/BankDebitModal.res @@ -1,5 +1,6 @@ open CardUtils open ACHTypes +open DynamicFieldsUtils type focus = Routing | Account | NONE @@ -118,6 +119,8 @@ let make = (~setModalData) => { let (requiredFieldsBody, setRequiredFieldsBody) = React.useState(_ => Dict.make()) + let paymentMethodListValue = Recoil.useRecoilValueFromAtom(PaymentUtils.paymentMethodListValue) + let (openModal, setOpenModal) = React.useState(_ => false) let (accountNum, setAccountNum) = React.useState(_ => "") @@ -176,6 +179,39 @@ let make = (~setModalData) => { let isAchDebit = selectedOption->String.includes("ach_debit") let isBecsDebit = selectedOption->String.includes("becs_debit") + let paymentMethod = "bank_debit" + let paymentMethodType = switch (isAchDebit, isBecsDebit) { + | (true, _) => "ach" + | (_, true) => "becs" + | _ => "sepa" + } + + let paymentMethodTypes = PaymentUtils.usePaymentMethodTypeFromList( + ~paymentMethodListValue, + ~paymentMethod, + ~paymentMethodType, + ) + + let (superpositionMissingFields, initialValues, _) = useSuperpositionFields( + ~paymentMethod, + ~paymentMethodType, + ~paymentMethodTypes, + ~paymentMethodListValue, + ) + + let getRequiredFieldPath = (fieldType: PaymentMethodsRecord.paymentMethodsFields) => { + superpositionMissingFields + ->Array.find(r => { + let convertedFieldType = DynamicFieldsUtils.fieldTypeToPaymentMethodsField( + r.fieldType, + r.options, + ) + convertedFieldType === fieldType + }) + ->Option.map(r => r.outputPath) + ->Option.getOr(getBillingAddressPathFromFieldType(fieldType)) + } + let handleAccountHolderNameChange = ev => { let accName = ReactEvent.Form.target(ev)["value"] setAccountHolderName(_ => accName) @@ -204,67 +240,60 @@ let make = (~setModalData) => {
+ let formValidator = React.useMemo(() => { + _ => Dict.make() + }, []) + let nonDynamicFieldsModalBody = - <> -
- {React.string(localeString.billingDetailsText)} -
-
- -
-
- {React.string("Bank Details")} -
-
- {React.string("Account Holder Name")} -
- setInputFocus(_ => NONE)} - /> - -
- {React.string("IBAN")} -
- -
-
- -
+ ()} + initialValues={Some(initialValues)} + validate={Some(formValidator)} + render={_ => { + <> +
+ {React.string(localeString.billingDetailsText)} +
+
+ +
+
+ {React.string("Bank Details")} +
+
+ {React.string("Account Holder Name")} +
+ setInputFocus(_ => NONE)} + /> +
{ color: themeObj.colorText, marginBottom: "5px", }> - {React.string("Routing number")} + {React.string("IBAN")}
setInputFocus(_ => Routing)} + value=iban + onChange=changeIBAN + type_="text" + maxLength=42 + inputRef=ibanRef + placeholder="eg: DE00 0000 0000 0000 0000 00" /> +
+
+ +
+
+ {React.string("Routing number")} +
+ setInputFocus(_ => Routing)} + /> +
+
+ +
+
+ {React.string("Account number")} +
+ setInputFocus(_ => Account)} + onBlur={_ => setInputFocus(_ => NONE)} + /> +
+
- - -
+ +
+ +
+
+
{ color: themeObj.colorText, marginBottom: "5px", }> - {React.string("Account number")} + {React.string("BSB")}
setInputFocus(_ => Account)} - onBlur={_ => setInputFocus(_ => NONE)} + maxLength=7 + placeholder="eg: 000-000" /> -
-
-
- -
- +
-
- -
- {React.string("BSB")} -
- -
-