From 610d250e66678810c499c14c3d4c2e8c3a00609b Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 16 Mar 2026 17:21:05 +0530 Subject: [PATCH] feat: superposition --- package-lock.json | 44 + package.json | 1 + src/Components/AddressPaymentInput.res | 484 ++++-- src/Components/BillingNamePaymentInput.res | 78 +- src/Components/BlikCodePaymentInput.res | 63 +- src/Components/CryptoCurrencyNetworks.res | 21 +- src/Components/DocumentNumberInput.res | 19 +- src/Components/DynamicFields.res | 1496 ++++++++++------- src/Components/EmailPaymentInput.res | 66 +- src/Components/FullNamePaymentInput.res | 288 +++- src/Components/GiftCardNumberInput.res | 56 +- src/Components/GiftCardPinInput.res | 56 +- src/Components/ManageSavedItem.res | 38 +- src/Components/NicknamePaymentInput.res | 33 +- src/Components/PhoneNumberPaymentInput.res | 202 ++- src/Components/PixPaymentInput.res | 132 +- 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 | 4 +- 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 | 34 +- src/Payments/BacsBankDebit.res | 36 +- src/Payments/BankDebitModal.res | 37 + src/Payments/BecsBankDebit.res | 35 +- src/Payments/DateOfBirth.res | 56 +- src/Payments/PaymentMethodsRecord.res | 15 +- src/Utilities/DynamicFieldsUtils.res | 1095 +++++++----- src/Utilities/Utils.res | 2 + 44 files changed, 2800 insertions(+), 1685 deletions(-) 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/src/Components/AddressPaymentInput.res b/src/Components/AddressPaymentInput.res index 753028e73..6330de07b 100644 --- a/src/Components/AddressPaymentInput.res +++ b/src/Components/AddressPaymentInput.res @@ -29,140 +29,107 @@ 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, + ) + + // Use RFF useField for each address field + 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(""))), + }, + ) + + // Get state names based on selected country + 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: "", - }) - } + // Check if there are default values to show other fields + 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: "", - }) + // Helper to get error string for a field + 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, - }) + "" } } + // Country change handler - reset state when country changes 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 +138,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 +165,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 +209,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 +230,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 @@ -253,3 +252,230 @@ let make = (~className="", ~paymentType: option=?) => {
} + + +// Legacy Recoil-based version for backward compatibility +// @react.component +// let make = (~className="", ~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 (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 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 getFieldError = (val, rule) => +// Validation.validateField( +// val, +// [rule, MaxLength(255)], +// ~enabledCardSchemes=[], +// ~localeObject=localeString->Obj.magic, +// )->Option.getOr("") + +// let checkPostalValidity = ( +// postal: RecoilAtomTypes.field, +// setPostal: (RecoilAtomTypes.field => RecoilAtomTypes.field) => unit, +// ) => { +// let err = getFieldError(postal.value, Validation.PostalCode(country.value)) +// setPostal(prev => { +// ...prev, +// isValid: Some(err === ""), +// errorString: err, +// }) +// } + +// let onPostalChange = ev => { +// let val = ReactEvent.Form.target(ev)["value"] +// setPostalCode(prev => { +// ...prev, +// value: val, +// errorString: "", +// }) +// } + +// let onPostalBlur = ev => { +// let val = ReactEvent.Focus.target(ev)["value"] +// let err = getFieldError(val, Validation.PostalCode(country.value)) +// setPostalCode(prev => { +// ...prev, +// isValid: Some(err === ""), +// errorString: err, +// }) +// } + +// React.useEffect(() => { +// checkPostalValidity(postalCode, setPostalCode) +// setState(prev => { +// ...prev, +// value: "", +// }) + +// 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, +// isValid: Some(false), +// errorString: getFieldError(line1.value, Validation.Required), +// }) +// } +// if line2.value == "" { +// setLine2(prev => { +// ...prev, +// isValid: Some(false), +// errorString: getFieldError(line2.value, Validation.Required), +// }) +// } +// if state.value == "" { +// setState(prev => { +// ...prev, +// isValid: Some(false), +// errorString: getFieldError(state.value, Validation.Required), +// }) +// } +// if postalCode.value == "" { +// setPostalCode(prev => { +// ...prev, +// isValid: Some(false), +// errorString: getFieldError(postalCode.value, Validation.PostalCode(country.value)), +// }) +// } +// if city.value == "" { +// setCity(prev => { +// ...prev, +// isValid: Some(false), +// errorString: getFieldError(city.value, Validation.Required), +// }) +// } +// } +// }, (line1, line2, country, state, city, postalCode)) +// useSubmitPaymentData(submitCallback) + +// let hasDefaulltValues = +// line2.value !== "" || city.value !== "" || postalCode.value !== "" || state.value !== "" + +//
+// +// { +// setShowOtherFields(_ => true) +// setLine1(prev => { +// ...prev, +// value: ReactEvent.Form.target(ev)["value"], +// }) +// }} +// type_="text" +// name="line1" +// className +// inputRef=line1Ref +// placeholder=localeString.line1Placeholder +// paymentType +// /> +// +// +//
+// +// { +// setLine2(prev => { +// ...prev, +// value: ReactEvent.Form.target(ev)["value"], +// }) +// }} +// type_="text" +// name="line2" +// className +// inputRef=line2Ref +// placeholder=localeString.line2Placeholder +// paymentType +// /> +// +//
+// +// +// +// Array.length > 0}> +// +// +//
+//
+// +// { +// setCity(prev => { +// ...prev, +// value: ReactEvent.Form.target(ev)["value"], +// }) +// }} +// type_="text" +// name="city" +// inputRef=cityRef +// placeholder=localeString.cityLabel +// paymentType +// /> +// +// +// +// +//
+//
+//
+//
+// } 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..b3480dbf0 100644 --- a/src/Components/DynamicFields.res +++ b/src/Components/DynamicFields.res @@ -1,3 +1,24 @@ +module RffField = { + @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) + } +} + module DynamicFieldsToRenderWrapper = { @react.component let make = (~children, ~index, ~isInside=true) => { @@ -35,6 +56,7 @@ let make = ( open PaymentTypeContext open Utils open RecoilAtoms + let paymentMethodListValue = Recoil.useRecoilValueFromAtom(PaymentUtils.paymentMethodListValue) let paymentManagementListValue = Recoil.useRecoilValueFromAtom( PaymentUtils.paymentManagementListValue, @@ -80,48 +102,16 @@ let make = ( ~paymentMethodType="credit", ) + let (superpositionMissingFields, initialValues, _) = useSuperpositionFields( + ~paymentMethod, + ~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, - )) + superpositionMissingFields + }, [superpositionMissingFields]) let requiredFields = React.useMemo(() => { requiredFieldsWithBillingDetails @@ -129,6 +119,45 @@ let make = ( ->removeClickToPayFieldsIfSaveDetailsWithClickToPay(isSaveDetailsWithClickToPay) }, (requiredFieldsWithBillingDetails, isSaveDetailsWithClickToPay)) + // TODO: cleanup by removing this extra filtering logic is required. + let getRequiredFieldPath = (fieldType: PaymentMethodsRecord.paymentMethodsFields) => { + // First try exact match + let exactMatch = requiredFields->Array.find(r => r.field_type === fieldType) + + switch exactMatch { + | Some(r) => + // For Email, prefer the billing path ("payment_method_data.billing.email") over bare paths + // (e.g. "payment_method_data.email") since both may exist in requiredFields with the same field_type. + switch fieldType { + | Email => + requiredFields + ->Array.find(e => + e.field_type === Email && e.required_field->String.includes("billing.email") + ) + ->Option.map(e => e.required_field) + ->Option.getOr(r.required_field) + | _ => r.required_field + } + | None => + // Handle the case where AddressCountry lookup fails due to array reference inequality + // When CountryAndPincode is rendered, it passes a newly created AddressCountry variant + // which doesn't match the one in requiredFields due to different array references + switch fieldType { + | AddressCountry(_) => + requiredFields + ->Array.find(r => + switch r.field_type { + | AddressCountry(_) => true + | _ => false + } + ) + ->Option.map(r => r.required_field) + ->Option.getOr("") + | _ => "" + } + } + } + let isAllStoredCardsHaveName = React.useMemo(() => { PaymentType.getIsStoredPaymentMethodHasName(savedMethod) }, [savedMethod]) @@ -258,58 +287,65 @@ let make = ( None }) - let onPostalChange = ev => { - let val = ReactEvent.Form.target(ev)["value"] + // 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: "", - }) - } - } + // if val !== "" { + // setPostalCode(_ => { + // isValid: Some(true), + // value: val, + // errorString: "", + // }) + // } else { + // setPostalCode(_ => { + // isValid: Some(false), + // value: val, + // errorString: "", + // }) + // } + // } - useRequiredFieldsEmptyAndValid( - ~requiredFields, - ~fieldsArr, - ~countryNames, - ~bankNames, - ~isCardValid, - ~isExpiryValid, - ~isCVCValid, - ~cardNumber, - ~cardExpiry, - ~cvcNumber, - ~isSavedCardFlow, - ~isSplitPaymentsEnabled, - ) + // useRequiredFieldsEmptyAndValid( + // ~requiredFields, + // ~fieldsArr, + // ~countryNames, + // ~bankNames, + // ~isCardValid, + // ~isExpiryValid, + // ~isCVCValid, + // ~cardNumber, + // ~cardExpiry, + // ~cvcNumber, + // ~isSavedCardFlow, + // ~isSplitPaymentsEnabled, + // ) - useSetInitialRequiredFields( - ~requiredFields={ - billingAddress.usePrefilledValues === Auto ? requiredFieldsWithBillingDetails : requiredFields - }, - ~paymentMethodType, - ) + // useSetInitialRequiredFields( + // ~requiredFields={ + // billingAddress.usePrefilledValues === Auto ? requiredFieldsWithBillingDetails : requiredFields + // }, + // ~paymentMethodType, + // ) - useRequiredFieldsBody( - ~requiredFields, - ~paymentMethodType, - ~cardNumber, - ~cardExpiry, - ~cvcNumber, - ~isSavedCardFlow, - ~isAllStoredCardsHaveName, - ~setRequiredFieldsBody, - ) + // useRequiredFieldsBody( + // ~requiredFields, + // ~paymentMethodType, + // ~cardNumber, + // ~cardExpiry, + // ~cvcNumber, + // ~isSavedCardFlow, + // ~isAllStoredCardsHaveName, + // ~setRequiredFieldsBody, + // ) + + let formSubmitRef = React.useRef(None) - let submitCallback = useSubmitCallback() + let submitCallback = useSubmitCallback(~onConfirm=() => { + switch formSubmitRef.current { + | Some(submitFn) => submitFn() + | None => () + } + }) useSubmitPaymentData(submitCallback) let bottomElement = @@ -329,6 +365,19 @@ let make = ( } } + let (firstNamePath, lastNamePath) = React.useMemo(() => { + let fullNameFields = + requiredFields->Array.filter((r: PaymentMethodsRecord.required_fields) => + r.field_type === FullName + ) + let findPath = suffix => + fullNameFields + ->Array.find(r => r.required_field->String.endsWith(suffix)) + ->Option.map(r => r.required_field) + ->Option.getOr("") + (findPath("first_name"), findPath("last_name")) + }, [requiredFields]) + let dynamicFieldsToRenderOutsideBilling = React.useMemo(() => { fieldsArr->Array.filter(isFieldTypeToRenderOutsideBilling) }, [fieldsArr]) @@ -344,528 +393,773 @@ let make = ( let spacedStylesForBiilingDetails = isSpacedInnerLayout ? "p-2" : "my-2" + let formValidator = React.useMemo(() => { + _ => Dict.make() + }, [fieldsArr]) + + let (_, setAreRequiredFieldsValid) = Recoil.useRecoilState(areRequiredFieldsValid) + + // let (formMethods, setFormMethods) = React.useState(_ => None) + 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} + ()} + initialValues={Some(initialValues)} + validate={Some(formValidator)} + render={formProps => { + formSubmitRef.current = Some(formProps.form.submit) + let submitFailed = formProps.submitFailed + // React.useEffect0(() => { + // setFormMethods(_ => Some(formProps)) + // None + // }) + ReactFinalForm.useFormStateHandler( + ~onFormChange=values => { + setRequiredFieldsBody(_ => values) + }, + ~onValidationChange=isValid => { + setAreRequiredFieldsValid(_ => isValid) + }, + ~formProps, + ) + <> + {dynamicFieldsToRenderOutsideBilling + ->Array.mapWithIndex((item, index) => { + Int.toString} index={index} isInside={false}> + {switch item { + | CardNumber => + + | GiftCardNumber => + | CardExpiryMonth + | CardExpiryYear + | CardExpiryMonthAndYear => + + | CardCvc => + + | GiftCardPin => + + | CardExpiryAndCvc => +
+ +
- - 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"} - /> - Array.length > 0}> - - -
- | CountryAndPincode(countryArr) => - let updatedCountryArray = - countryArr->DropdownField.updateArrayOfStringToOptionsTypeArray -
+ | Currency(currencyArr) => + let updatedCurrencyArray = + currencyArr->DropdownField.updateArrayOfStringToOptionsTypeArray + { + let val = field.input.value->Option.getOr(currency) - { - let value = ReactEvent.Focus.target(ev)["value"] - setPostalCode(prev => { - ...prev, - isValid: Some(value !== ""), - }) + fieldName=localeString.currencyLabel + value=val + setValue={setter => { + let newVal = setter(val) + setCurrency(_ => newVal) + field.input.onChange(newVal) }} - onChange=onPostalChange - name="postal" - inputRef=postalRef - placeholder=localeString.postalCodeLabel - className={isSpacedInnerLayout ? "" : "!border-t-0"} + disabled=false + options=updatedCurrencyArray /> -
- | 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"} + }} + /> + | DocumentType(opt) => { + let updatedDocumentTypeArray = + opt->DropdownField.updateArrayOfStringToOptionsTypeArrayWithUpperCaseLabel + - | 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 + } + | FullName => + let defaultName = + paymentMethod === "card" + ? localeString.cardHolderName + : localeString.fullNameLabel + let customName = item->getCustomFieldName->Option.getOr(defaultName) + <> + +
+ {customName->React.string} +
+
+ // TODO: rename properly + - | AddressState => - Array.length > 0}> - + | CryptoCurrencyNetworks => + + | DateOfBirth => + | VpaId => + | PixKey => + | PixCPF => + | PixCNPJ => + + | BankAccountNumber | IBAN => + { + 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="bankAccountNumber" + maxLength=42 + inputRef=bankAccountNumberRef + placeholder="DE00 0000 0000 0000 0000 00" /> - - | AddressPincode => - { - let value = ReactEvent.Focus.target(ev)["value"] - setPostalCode(prev => { - ...prev, - isValid: Some(value !== ""), - }) + }} + /> + | SourceBankAccountId => + { + 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" + /> + }} + /> + | 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 => + { + 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=TestUtils.cardHolderNameInputTestId + inputRef={React.useRef(Nullable.null)} + placeholder=localeString.billingNamePlaceholder + className={isSpacedInnerLayout ? "" : "!border-b-0"} + /> + }} + /> + | Email => + { + 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" + /> + + }} + /> + | PhoneNumberAndCountryCode => + // TODO: rename properly + + | StateAndCity => +
+ { + 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"} + /> + }} + /> + Array.length > 0}> + { + let val = field.input.value->Option.getOr("") + Option.getOr("") + : "", + } + setValue={setter => { + let newVal = setter({ + value: val, + isValid: Some(field.meta.valid), + errorString: "", + }) + field.input.onChange(newVal.value) + }} + options={stateNames} + /> + }} + /> + +
+ | CountryAndPincode(countryArr) => + let updatedCountryArray = + countryArr->DropdownField.updateArrayOfStringToOptionsTypeArray +
+ { + // let val = field.input.value->Option.getOr(country) + { + 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"} + /> + }} + /> +
+ | AddressLine1 => + { + 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"} + /> + }} + /> + | AddressLine2 => + { + 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 + /> + }} + /> + | AddressCity => + { + 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 + /> + }} + /> + | AddressState => + Array.length > 0}> + { + let val = field.input.value->Option.getOr("") + Option.getOr("") + : "", + } + setValue={setter => { + let newVal = setter({ + value: val, + isValid: Some(field.meta.valid), + errorString: "", + }) + field.input.onChange(newVal.value) + }} + options={stateNames} + /> + }} + /> + + | AddressPincode => + { + 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 => { + let newVal = ReactEvent.Form.target(ev)["value"] + setPostalCode(prev => { + ...prev, + value: newVal, + isValid: Some(newVal !== ""), + }) + field.input.onChange(newVal) + }} + onBlur={_ev => field.input.onBlur()} + name="postal" + inputRef=postalRef + placeholder=localeString.postalCodeLabel + /> + }} + /> + | BlikCode => + | Country => + let updatedCountryNames = + countryNames->DropdownField.updateArrayOfStringToOptionsTypeArray + { + // let val = field.input.value->Option.getOr(country) + { + let newVal = setter(field.input.value->Option.getOr(country)) + setCountry(_ => newVal) + field.input.onChange(newVal) + }} + disabled=false + options=updatedCountryNames + /> + }} + /> + | AddressCountry(countryArr) => + let updatedCountryArr = + countryArr->DropdownField.updateArrayOfStringToOptionsTypeArray + { + // let val = field.input.value->Option.getOr(country) + { + let newVal = setter(field.input.value->Option.getOr(country)) + setCountry(_ => newVal) + field.input.onChange(newVal) + }} + disabled=false + options=updatedCountryArr + /> + }} + /> + | BankList(bankArr) => + let updatedBankNames = + Bank.getBanks(paymentMethodType) + ->getBankNames(bankArr) + ->DropdownField.updateArrayOfStringToOptionsTypeArray + { + let val = field.input.value->Option.getOr(selectedBank) + { + let newVal = setter(val) + setSelectedBank(_ => newVal) + field.input.onChange(newVal) + }} + disabled=false + options=updatedBankNames + /> + }} + /> + | Bank => + 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 + /> + }} + /> + | 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 }} - 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 fieldsArr->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..120ae4331 100644 --- a/src/Components/FullNamePaymentInput.res +++ b/src/Components/FullNamePaymentInput.res @@ -1,76 +1,232 @@ open RecoilAtoms open PaymentType -open Utils + +// module FieldSpy = { +// @react.component +// let make = (~name) => { +// let field = ReactFinalForm.useField(name) +// let {submitFailed} = ReactFinalForm.useFormState() + +// React.useEffect(() => { +// let fieldVal = field.input.value->Option.getOr("") +// let fieldErr = field.meta.error->Option.getOr("None") +// let fieldTouched = field.meta.touched ? "true" : "false" +// let fieldSubmitFailed = submitFailed ? "true" : "false" +// let fieldValid = field.meta.valid ? "true" : "false" + +// Console.log6( +// `-- FieldSpy [${name}]:`, +// `-- FieldSpy field.input.value: "${fieldVal}",`, +// `-- FieldSpy field.meta.error: "${fieldErr}",`, +// `-- FieldSpy field.meta.touched: ${fieldTouched},`, +// `-- FieldSpy submitFailed: ${fieldSubmitFailed},`, +// `-- FieldSpy field.meta.valid: ${fieldValid}`, +// ) +// None +// }, (field.input.value, field.meta.error, field.meta.touched, submitFailed)) + +// React.null +// } +// } @react.component -let make = (~customFieldName=None, ~optionalRequiredFields=None) => { - 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)) - } +module RffFullNamePaymentInput = { + @react.component + let make = (~customFieldName, ~firstNamePath, ~lastNamePath) => { + let {localeString} = Recoil.useRecoilValueFromAtom(configAtom) + let {fields} = Recoil.useRecoilValueFromAtom(optionAtom) + let formState = ReactFinalForm.useFormState() + let submitFailed = formState.submitFailed - let onBlur = ev => { - let val: string = ReactEvent.Focus.target(ev)["value"] - setFullName(prev => validateName(val, prev, localeString)) - } + let (placeholder, fieldName) = switch customFieldName { + | Some(val) => (val, val) + | None => (localeString.fullNamePlaceholder, localeString.fullNameLabel) + } - let (placeholder, fieldName) = switch customFieldName { - | Some(val) => (val, val) - | None => (localeString.fullNamePlaceholder, localeString.fullNameLabel) - } - let nameRef = React.useRef(Nullable.null) - - React.useEffect(() => { - setFullName(prev => validateName(prev.value, prev, localeString)) - None - }, []) - - 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, - }) + let createValidator = rule => + Validation.createFieldValidator( + rule, + ~enabledCardSchemes=[], + ~localeObject=localeString->Obj.magic, + ) + + // let (firstNamePath, lastNamePath) = React.useMemo(() => { + // switch optionalRequiredFields { + // | Some(requiredFields) => + // let fullNameFields = + // requiredFields->Array.filter((r: PaymentMethodsRecord.required_fields) => + // r.field_type === FullName + // ) + // let findPath = suffix => + // fullNameFields + // ->Array.find(r => r.required_field->String.endsWith(suffix)) + // ->Option.map(r => r.required_field) + // (findPath("first_name"), findPath("last_name")) + // | None => (None, None) + // } + // }, [optionalRequiredFields]) + + 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 { - switch optionalRequiredFields { - | Some(requiredFields) => - if !DynamicFieldsUtils.checkIfNameIsValid(requiredFields, FullName, fullName) { - setFullName(prev => { - ...prev, - errorString: fieldName->localeString.completeNameEmptyText, - }) - } - | None => () - } + 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) } } - }, [fullName]) - useSubmitPaymentData(submitCallback) - - - - + + let onBlur = (_ev: JsxEventU.Focus.t) => { + firstField.input.onBlur() + lastField.input.onBlur() + } + + let nameRef = React.useRef(Nullable.null) + + // Show an error if either field has one and the user has touched the input. + let errorString = if ( + // submitFailed || + 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 + | _ => "" + } + } else { + "" + } + + let isValid = + // !submitFailed && + 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 + placeholder + name=TestUtils.fullNameInputTestId + /> + + } } + +// @react.component +// let make = (~name=?, ~customFieldName=None, ~firstNamePath=?, ~lastNamePath=?) => { +// switch (name, firstNamePath, lastNamePath) { +// | (Some(name), Some(fnPath), Some(lnPath)) => +// +// | _ => +// 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 onBlur = ev => { +// let val: string = ReactEvent.Focus.target(ev)["value"] +// setFullName(prev => validateName(val, prev, localeString)) +// } + +// let (placeholder, fieldName) = switch customFieldName { +// | Some(val) => (val, val) +// | None => (localeString.fullNamePlaceholder, localeString.fullNameLabel) +// } +// let nameRef = React.useRef(Nullable.null) + +// React.useEffect(() => { +// setFullName(prev => validateName(prev.value, prev, localeString)) +// None +// }, [localeString]) + +// 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: localeString.nameEmptyText(fieldName), +// }) +// } 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: localeString.completeNameEmptyText(fieldName), +// }) +// } +// | None => () +// } +// } +// } +// }, (fullName, localeString, fieldName, optionalRequiredFields)) +// useSubmitPaymentData(submitCallback) + +// +// +// +// } +// } 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..78afcf16b 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,37 @@ let make = ( | _ => "" } + let paymentMethodListValue = Recoil.useRecoilValueFromAtom(PaymentUtils.paymentMethodListValue) + + let paymentMethod = "card" + let paymentMethodType = "credit" + + let paymentMethodTypes = PaymentUtils.usePaymentMethodTypeFromList( + ~paymentMethodListValue, + ~paymentMethod, + ~paymentMethodType, + ) + + let (superpositionMissingFields, _, _) = useSuperpositionFields( + ~paymentMethod, + ~paymentMethodType, + ~paymentMethodTypes, + ~paymentMethodListValue, + ) + + let (firstNamePath, lastNamePath) = React.useMemo(() => { + let fullNameFields = + superpositionMissingFields->Array.filter((r: PaymentMethodsRecord.required_fields) => + r.field_type === FullName + ) + let findPath = suffix => + fullNameFields + ->Array.find(r => r.required_field->String.endsWith(suffix)) + ->Option.map(r => r.required_field) + ->Option.getOr("") + (findPath("first_name"), findPath("last_name")) + }, [superpositionMissingFields]) + React.useEffect(() => { startTransition(() => { setFullName(prev => Utils.validateName(cardHolderName, prev, localeString)) @@ -81,7 +113,11 @@ let make = ( />
- + } 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..554680213 100644 --- a/src/Components/PhoneNumberPaymentInput.res +++ b/src/Components/PhoneNumberPaymentInput.res @@ -1,104 +1,124 @@ -@react.component -let make = () => { - open RecoilAtoms - open PaymentType - open Utils +module RffPhoneNumberPaymentInput = { + @react.component + let make = (~numberName: string, ~codeName: string) => { + open RecoilAtoms + open PaymentType + open Utils - 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) - let (displayValue, setDisplayValue) = React.useState(_ => "") + let {localeString} = Recoil.useRecoilValueFromAtom(configAtom) - let countryAndCodeCodeList = - phoneNumberJson - ->JSON.Decode.object - ->Option.getOr(Dict.make()) - ->getArray("countries") + let createValidator = rule => + Validation.createFieldValidator( + rule, + ~enabledCardSchemes=[], + ~localeObject=localeString->Obj.magic, + ) - let phoneNumberCodeOptions: array< - DropdownField.optionType, - > = countryAndCodeCodeList->Array.reduce([], (acc, countryObj) => { - let countryObjDict = countryObj->getDictFromJson - let countryFlag = countryObjDict->getString("country_flag", "") - let phoneNumberCode = countryObjDict->getString("phone_number_code", "") - let countryName = countryObjDict->getString("country_name", "") + let numberField: ReactFinalForm.Field.fieldProps = ReactFinalForm.useField( + numberName, + ~config={validate: createValidator(Validation.Phone)}, + ) + let codeField: ReactFinalForm.Field.fieldProps = ReactFinalForm.useField(codeName) - let phoneNumberOptionsValue: DropdownField.optionType = { - label: `${countryFlag} ${countryName} ${phoneNumberCode}`, - displayValue: `${countryFlag} ${phoneNumberCode}`, - value: `${countryFlag}#${phoneNumberCode}`, - } - acc->Array.push(phoneNumberOptionsValue) - acc - }) + let numberVal = numberField.input.value->Option.getOr("") + let codeVal = codeField.input.value->Option.getOr("") - let defaultCountryCodeFilteredValue = - countryAndCodeCodeList - ->Array.filter(countryObj => { - countryObj->getDictFromJson->getString("country_code", "") === currentCountryCode.isoAlpha2 - }) - ->Array.get(0) - ->Option.getOr( - { - "phone_number_code": "", - }->Identity.anyTypeToJson, - ) - ->getDictFromJson - ->getString("phone_number_code", "") + let phoneRef = React.useRef(Nullable.null) + let {fields} = Recoil.useRecoilValueFromAtom(optionAtom) + let showDetails = getShowDetails(~billingDetails=fields.billingDetails) + let clientTimeZone = CardUtils.dateTimeFormat().resolvedOptions().timeZone + let clientCountry = getClientCountry(clientTimeZone) + let currentCountryCode = Utils.getCountryCode(clientCountry.countryName) + let (displayValue, setDisplayValue) = React.useState(_ => "") - let (valueDropDown, setValueDropDown) = React.useState(_ => defaultCountryCodeFilteredValue) - let getCountryCodeSplitValue = val => val->String.split("#")->Array.get(1)->Option.getOr("") + let countryAndCodeCodeList = + phoneNumberJson + ->JSON.Decode.object + ->Option.getOr(Dict.make()) + ->getArray("countries") - 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 phoneNumberCodeOptions: array< + DropdownField.optionType, + > = countryAndCodeCodeList->Array.reduce([], (acc, countryObj) => { + let countryObjDict = countryObj->getDictFromJson + let countryFlag = countryObjDict->getString("country_flag", "") + let phoneNumberCode = countryObjDict->getString("phone_number_code", "") + let countryName = countryObjDict->getString("country_name", "") - React.useEffect(() => { - setPhone(prev => { - ...prev, - countryCode: valueDropDown->getCountryCodeSplitValue, + let phoneNumberOptionsValue: DropdownField.optionType = { + label: `${countryFlag} ${countryName} ${phoneNumberCode}`, + displayValue: `${countryFlag} ${phoneNumberCode}`, + value: `${countryFlag}#${phoneNumberCode}`, + } + acc->Array.push(phoneNumberOptionsValue) + acc }) - None - }, [valueDropDown]) - React.useEffect(() => { - let findDisplayValue = - phoneNumberCodeOptions - ->Array.find(ele => ele.value === valueDropDown) - ->Option.getOr(DropdownField.defaultValue) - setDisplayValue(_ => - findDisplayValue.displayValue->Option.getOr( - findDisplayValue.label->Option.getOr(findDisplayValue.value), + let defaultCountryCodeFilteredValue = + countryAndCodeCodeList + ->Array.filter(countryObj => { + countryObj->getDictFromJson->getString("country_code", "") === currentCountryCode.isoAlpha2 + }) + ->Array.get(0) + ->Option.getOr( + { + "phone_number_code": "", + }->Identity.anyTypeToJson, ) - ) - None - }, [phoneNumberCodeOptions]) + ->getDictFromJson + ->getString("phone_number_code", "") - - - + let (valueDropDown, setValueDropDown) = React.useState(_ => defaultCountryCodeFilteredValue) + 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"), "") + numberField.input.onChange(val) + codeField.input.onChange(valueDropDown->getCountryCodeSplitValue) + } + + React.useEffect(() => { + codeField.input.onChange(valueDropDown->getCountryCodeSplitValue) + None + }, [valueDropDown]) + + React.useEffect(() => { + let findDisplayValue = + phoneNumberCodeOptions + ->Array.find(ele => ele.value === valueDropDown) + ->Option.getOr(DropdownField.defaultValue) + setDisplayValue(_ => + findDisplayValue.displayValue->Option.getOr( + findDisplayValue.label->Option.getOr(findDisplayValue.value), + ) + ) + None + }, [phoneNumberCodeOptions]) + + + Option.getOr("") : "", + } + setValue={_ => ()} + onChange=changePhone + paymentType=Payment + type_="tel" + name="phone" + inputRef=phoneRef + placeholder="000 000 000" + maxLength=14 + dropDownOptions=phoneNumberCodeOptions + valueDropDown + setValueDropDown + displayValue + setDisplayValue + /> + + } } 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/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..da07a74f6 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..8fc1fba6d 100644 --- a/src/Payments/ACHBankDebit.res +++ b/src/Payments/ACHBankDebit.res @@ -1,6 +1,6 @@ open RecoilAtoms open Utils -open PaymentModeType +open DynamicFieldsUtils @react.component let make = () => { @@ -106,13 +106,43 @@ let make = () => { let paymentMethodType = "ach" let paymentMethod = "bank_debit" + let paymentMethodTypes = PaymentUtils.usePaymentMethodTypeFromList( + ~paymentMethodListValue, + ~paymentMethod, + ~paymentMethodType, + ) + + let (superpositionMissingFields, _, _) = useSuperpositionFields( + ~paymentMethod, + ~paymentMethodType, + ~paymentMethodTypes, + ~paymentMethodListValue, + ) + + let (firstNamePath, lastNamePath) = React.useMemo(() => { + let fullNameFields = + superpositionMissingFields->Array.filter((r: PaymentMethodsRecord.required_fields) => + r.field_type === FullName + ) + let findPath = suffix => + fullNameFields + ->Array.find(r => r.required_field->String.endsWith(suffix)) + ->Option.map(r => r.required_field) + ->Option.getOr("") + (findPath("first_name"), findPath("last_name")) + }, [superpositionMissingFields]) + <>
- +
diff --git a/src/Payments/BacsBankDebit.res b/src/Payments/BacsBankDebit.res index 1d97cb76d..19876d6a5 100644 --- a/src/Payments/BacsBankDebit.res +++ b/src/Payments/BacsBankDebit.res @@ -1,6 +1,7 @@ open RecoilAtoms open RecoilAtomTypes open Utils +open DynamicFieldsUtils let formatSortCode = sortcode => { let formatted = sortcode->String.replaceRegExp(%re("/\D+/g"), "") @@ -135,6 +136,26 @@ let make = () => { let paymentMethodType = "bacs" let paymentMethod = "bank_debit" + let paymentMethodTypes = PaymentUtils.usePaymentMethodTypeFromList( + ~paymentMethodListValue, + ~paymentMethod, + ~paymentMethodType, + ) + + let (superpositionMissingFields, _, _) = useSuperpositionFields( + ~paymentMethod, + ~paymentMethodType, + ~paymentMethodTypes, + ~paymentMethodListValue, + ) + + let getRequiredFieldPath = (fieldType: PaymentMethodsRecord.paymentMethodsFields) => { + superpositionMissingFields + ->Array.find(r => r.field_type === fieldType) + ->Option.map(r => r.required_field) + ->Option.getOr(getBillingAddressPathFromFieldType(fieldType)) + } + <> @@ -164,8 +185,19 @@ let make = () => { />
- - + +
diff --git a/src/Payments/BankDebitModal.res b/src/Payments/BankDebitModal.res index 119f19034..84b94d7f5 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,33 @@ 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, _, _) = useSuperpositionFields( + ~paymentMethod, + ~paymentMethodType, + ~paymentMethodTypes, + ~paymentMethodListValue, + ) + + let getRequiredFieldPath = (fieldType: PaymentMethodsRecord.paymentMethodsFields) => { + superpositionMissingFields + ->Array.find(r => r.field_type === fieldType) + ->Option.map(r => r.required_field) + ->Option.getOr(getBillingAddressPathFromFieldType(fieldType)) + } + let handleAccountHolderNameChange = ev => { let accName = ReactEvent.Form.target(ev)["value"] setAccountHolderName(_ => accName) @@ -204,6 +234,7 @@ let make = (~setModalData) => { + // TODO: to check why this was named as nonDynamicFieldsModalBody and if this is still required. let nonDynamicFieldsModalBody = <>
{
{ @@ -86,9 +87,41 @@ let make = () => { let paymentMethod = "bank_debit" let paymentMethodType = "becs" + let paymentMethodListValue = Recoil.useRecoilValueFromAtom(PaymentUtils.paymentMethodListValue) + + let paymentMethodTypes = PaymentUtils.usePaymentMethodTypeFromList( + ~paymentMethodListValue, + ~paymentMethod, + ~paymentMethodType, + ) + + let (superpositionMissingFields, _, _) = useSuperpositionFields( + ~paymentMethod, + ~paymentMethodType, + ~paymentMethodTypes, + ~paymentMethodListValue, + ) + + let (firstNamePath, lastNamePath) = React.useMemo(() => { + let fullNameFields = + superpositionMissingFields->Array.filter((r: PaymentMethodsRecord.required_fields) => + r.field_type === FullName + ) + let findPath = suffix => + fullNameFields + ->Array.find(r => r.required_field->String.endsWith(suffix)) + ->Option.map(r => r.required_field) + ->Option.getOr("") + (findPath("first_name"), findPath("last_name")) + }, [superpositionMissingFields]) +
- + diff --git a/src/Payments/DateOfBirth.res b/src/Payments/DateOfBirth.res index 051d324ca..ce856911e 100644 --- a/src/Payments/DateOfBirth.res +++ b/src/Payments/DateOfBirth.res @@ -20,40 +20,46 @@ let currentYear = Date.getFullYear(Date.make()) let years = Array.fromInitializer(~length=currentYear - startYear, i => currentYear - i) @react.component -let make = () => { +let make = (~name: string) => { open Utils let {themeObj, localeString} = Recoil.useRecoilValueFromAtom(RecoilAtoms.configAtom) - let (error, setError) = React.useState(_ => false) - let (isNotEligible, setIsNotEligible) = React.useState(_ => false) let loggerState = Recoil.useRecoilValueFromAtom(RecoilAtoms.loggerAtom) - let (dateOfBirth, setDateOfBirth) = Recoil.useRecoilState(RecoilAtoms.dateOfBirth) - let submitCallback = React.useCallback((ev: Window.event) => { - let json = ev.data->safeParse - let confirm = json->getDictFromJson->ConfirmType.itemToObjMapper - if confirm.doSubmit { - switch dateOfBirth->Nullable.toOption { - | Some(_) => setError(_ => false) - | None => () - } - } - }, (dateOfBirth, isNotEligible)) + let field: ReactFinalForm.Field.fieldProps = ReactFinalForm.useField( + name, + ~config={ + validate: val => { + let date = val->Option.map(Date.fromString) + switch date { + | Some(date) => + if date->checkIs18OrAbove { + None + } else { + Some(localeString.dateOfBirthInvalidText) + } + | None => Some(localeString.dateofBirthRequiredText) + } + }, + }, + ) - useSubmitPaymentData(submitCallback) + let dateOfBirth = + field.input.value + ->Option.map(Date.fromString) + ->Nullable.fromOption let onChange = date => { - let isAbove18 = switch date->Nullable.toOption { - | Some(val) => val->checkIs18OrAbove - | None => false - } LoggerUtils.logInputChangeInfo("dateOfBirth", loggerState) - setDateOfBirth(_ => date) - setIsNotEligible(_ => !isAbove18) + let valStr = + date + ->Nullable.toOption + ->Option.map(d => d->Date.toISOString) + ->Option.getOr("") + field.input.onChange(valStr) } - let errorString = error - ? localeString.dateofBirthRequiredText - : localeString.dateOfBirthInvalidText + let isNotEligible = field.meta.touched && field.meta.error->Option.isSome + let errorString = field.meta.error->Option.getOr("")
{
}} /> - +
), InfoElement], - icon: Some(icon("mbway", ~size=19)), - displayName: localeString.payment_methods_mb_way, - miniIcon: None, - }, + // TODO: to handle for mb_way + // { + // paymentMethodName: "mb_way", + // fields: [SpecialField(), InfoElement], + // icon: Some(icon("mbway", ~size=19)), + // displayName: localeString.payment_methods_mb_way, + // miniIcon: None, + // }, { paymentMethodName: "mobile_pay", fields: [InfoElement], diff --git a/src/Utilities/DynamicFieldsUtils.res b/src/Utilities/DynamicFieldsUtils.res index c22a49653..733e2ab8f 100644 --- a/src/Utilities/DynamicFieldsUtils.res +++ b/src/Utilities/DynamicFieldsUtils.res @@ -1,4 +1,5 @@ open RecoilAtoms +open SuperpositionTypes let dynamicFieldsEnabledPaymentMethods = [ "crypto_currency", @@ -401,409 +402,409 @@ let useRequiredFieldsEmptyAndValid = ( }, (isCardValid, isExpiryValid, isCVCValid, areRequiredFieldsValid)) } -let useSetInitialRequiredFields = ( - ~requiredFields: array, - ~paymentMethodType, -) => { - let (email, setEmail) = Recoil.useRecoilState(userEmailAddress) - let (fullName, setFullName) = Recoil.useRecoilState(userFullName) - let (billingName, setBillingName) = Recoil.useRecoilState(userBillingName) - let (line1, setLine1) = Recoil.useRecoilState(userAddressline1) - let (line2, setLine2) = Recoil.useRecoilState(userAddressline2) - let (phone, setPhone) = Recoil.useRecoilState(userPhoneNumber) - let (state, setState) = Recoil.useRecoilState(userAddressState) - let (city, setCity) = Recoil.useRecoilState(userAddressCity) - let (postalCode, setPostalCode) = Recoil.useRecoilState(userAddressPincode) - let (blikCode, setBlikCode) = Recoil.useRecoilState(userBlikCode) - let (pixCNPJ, setPixCNPJ) = Recoil.useRecoilState(userPixCNPJ) - let (pixCPF, setPixCPF) = Recoil.useRecoilState(userPixCPF) - let (pixKey, setPixKey) = Recoil.useRecoilState(userPixKey) - - let (country, setCountry) = Recoil.useRecoilState(userCountry) - let (selectedBank, setSelectedBank) = Recoil.useRecoilState(userBank) - let (currency, setCurrency) = Recoil.useRecoilState(userCurrency) - let (documentType, setDocumentType) = Recoil.useRecoilState(userDocumentType) - let (documentNumber, setDocumentNumber) = Recoil.useRecoilState(userDocumentNumber) - let (cryptoCurrencyNetworks, setCryptoCurrencyNetworks) = Recoil.useRecoilState( - cryptoCurrencyNetworks, - ) - let (dateOfBirth, setDateOfBirth) = Recoil.useRecoilState(dateOfBirth) - let (bankAccountNumber, setBankAccountNumber) = Recoil.useRecoilState(userBankAccountNumber) - let (sourceBankAccountId, setSourceBankAccountId) = Recoil.useRecoilState(sourceBankAccountId) - let (giftCardNumber, setGiftCardNumber) = Recoil.useRecoilState(userGiftCardNumber) - let (giftCardPin, setGiftCardPin) = Recoil.useRecoilState(userGiftCardPin) - - React.useEffect(() => { - let getNameValue = (item: PaymentMethodsRecord.required_fields) => { - requiredFields - ->Array.filter(requiredFields => requiredFields.field_type === item.field_type) - ->Array.reduce("", (acc, item) => { - let requiredFieldsArr = item.required_field->String.split(".") - switch requiredFieldsArr->Array.get(requiredFieldsArr->Array.length - 1)->Option.getOr("") { - | "first_name" => item.value->String.concat(acc) - | "last_name" => acc->String.concatMany([" ", item.value]) - | _ => acc - } - }) - ->String.trim - } - - let setFields = ( - setMethod: (RecoilAtomTypes.field => RecoilAtomTypes.field) => unit, - field: RecoilAtomTypes.field, - item: PaymentMethodsRecord.required_fields, - isNameField, - ~isCountryCodeAvailable=?, - ) => { - if isNameField && field.value === "" { - setMethod(prev => { - ...prev, - value: getNameValue(item), - }) - if isCountryCodeAvailable->Option.isSome { - setMethod(prev => { - ...prev, - countryCode: getNameValue(item), - }) - } - } else if field.value === "" { - if isCountryCodeAvailable->Option.isSome { - setMethod(prev => { - ...prev, - countryCode: item.value, - }) - } else { - setMethod(prev => { - ...prev, - value: item.value, - }) - } - } - } - - requiredFields->Array.forEach(requiredField => { - let value = requiredField.value - switch requiredField.field_type { - | Email => { - let emailValue = email.value - setFields(setEmail, email, requiredField, false) - if emailValue === "" { - let newEmail: RecoilAtomTypes.field = { - value, - isValid: None, - errorString: "", - } - Utils.checkEmailValid(newEmail, setEmail) - } - } - | FullName => setFields(setFullName, fullName, requiredField, true) - | AddressLine1 => setFields(setLine1, line1, requiredField, false) - | AddressLine2 => setFields(setLine2, line2, requiredField, false) - | StateAndCity => { - setFields(setState, state, requiredField, false) - setFields(setCity, city, requiredField, false) - } - | CountryAndPincode(_) => { - setFields(setPostalCode, postalCode, requiredField, false) - if value !== "" && country === "" { - let countryCode = - Country.getCountry(paymentMethodType, countryList) - ->Array.filter(item => item.isoAlpha2 === value) - ->Array.get(0) - ->Option.getOr(Country.defaultTimeZone) - setCountry(_ => countryCode.countryName) - } - } - | AddressState => setFields(setState, state, requiredField, false) - | GiftCardNumber => setFields(setGiftCardNumber, giftCardNumber, requiredField, false) - | GiftCardPin => setFields(setGiftCardPin, giftCardPin, requiredField, false) - | AddressCity => setFields(setCity, city, requiredField, false) - | PhoneCountryCode => - setFields(setPhone, phone, requiredField, false, ~isCountryCodeAvailable=true) - | AddressPincode => setFields(setPostalCode, postalCode, requiredField, false) - | PhoneNumber => setFields(setPhone, phone, requiredField, false) - | PhoneNumberAndCountryCode => - setFields(setPhone, phone, requiredField, false, ~isCountryCodeAvailable=true) - | BlikCode => setFields(setBlikCode, blikCode, requiredField, false) - | PixKey => setFields(setPixKey, pixKey, requiredField, false) - | PixCNPJ => setFields(setPixCNPJ, pixCNPJ, requiredField, false) - | PixCPF => setFields(setPixCPF, pixCPF, requiredField, false) - | BillingName => setFields(setBillingName, billingName, requiredField, true) - | Country - | AddressCountry(_) => - if value !== "" { - let defaultCountry = - Country.getCountry(paymentMethodType, countryList) - ->Array.filter(item => item.isoAlpha2 === value) - ->Array.get(0) - ->Option.getOr(Country.defaultTimeZone) - setCountry(_ => defaultCountry.countryName) - } - | Currency(_) => - if value !== "" && currency === "" { - setCurrency(_ => value) - } - | Bank => - if value !== "" && selectedBank === "" { - setSelectedBank(_ => value) - } - | CryptoCurrencyNetworks => - if value !== "" && cryptoCurrencyNetworks === "" { - setCryptoCurrencyNetworks(_ => value) - } - | DateOfBirth => - switch dateOfBirth->Nullable.toOption { - | Some(x) => - if value !== "" && x->Date.toDateString === "" { - setDateOfBirth(_ => Nullable.make(x)) - } - | None => () - } - | IBAN - | BankAccountNumber => - setFields(setBankAccountNumber, bankAccountNumber, requiredField, false) - | SourceBankAccountId => - setFields(setSourceBankAccountId, sourceBankAccountId, requiredField, false) - | DocumentType(_) => - if value !== "" && documentType === "" { - setDocumentType(_ => value) - } - | DocumentNumber => setFields(setDocumentNumber, documentNumber, requiredField, false) - | LanguagePreference(_) - | SpecialField(_) - | InfoElement - | CardNumber - | CardExpiryMonth - | CardExpiryYear - | CardExpiryMonthAndYear - | CardCvc - | CardExpiryAndCvc - | ShippingName // Shipping Details are currently supported by only one click widgets - | ShippingAddressLine1 - | ShippingAddressLine2 - | ShippingAddressCity - | ShippingAddressPincode - | ShippingAddressState - | ShippingAddressCountry(_) - | BankList(_) - | VpaId - | None => () - } - }) - None - }, [requiredFields]) -} - -let useRequiredFieldsBody = ( - ~requiredFields: array, - ~paymentMethodType, - ~cardNumber, - ~cardExpiry, - ~cvcNumber, - ~isSavedCardFlow, - ~isAllStoredCardsHaveName, - ~setRequiredFieldsBody, -) => { - let configValue = Recoil.useRecoilValueFromAtom(configAtom) - let email = Recoil.useRecoilValueFromAtom(userEmailAddress) - let vpaId = Recoil.useRecoilValueFromAtom(userVpaId) - let pixCNPJ = Recoil.useRecoilValueFromAtom(userPixCNPJ) - let pixCPF = Recoil.useRecoilValueFromAtom(userPixCPF) - let pixKey = Recoil.useRecoilValueFromAtom(userPixKey) - let fullName = Recoil.useRecoilValueFromAtom(userFullName) - let billingName = Recoil.useRecoilValueFromAtom(userBillingName) - let line1 = Recoil.useRecoilValueFromAtom(userAddressline1) - let line2 = Recoil.useRecoilValueFromAtom(userAddressline2) - let phone = Recoil.useRecoilValueFromAtom(userPhoneNumber) - let state = Recoil.useRecoilValueFromAtom(userAddressState) - let city = Recoil.useRecoilValueFromAtom(userAddressCity) - let postalCode = Recoil.useRecoilValueFromAtom(userAddressPincode) - let blikCode = Recoil.useRecoilValueFromAtom(userBlikCode) - let country = Recoil.useRecoilValueFromAtom(userCountry) - let selectedBank = Recoil.useRecoilValueFromAtom(userBank) - let currency = Recoil.useRecoilValueFromAtom(userCurrency) - let documentType = Recoil.useRecoilValueFromAtom(userDocumentType) - let documentNumber = Recoil.useRecoilValueFromAtom(userDocumentNumber) - let {billingAddress} = Recoil.useRecoilValueFromAtom(optionAtom) - let cryptoCurrencyNetworks = Recoil.useRecoilValueFromAtom(cryptoCurrencyNetworks) - let dateOfBirth = Recoil.useRecoilValueFromAtom(dateOfBirth) - let bankAccountNumber = Recoil.useRecoilValueFromAtom(userBankAccountNumber) - let sourceBankAccountId = Recoil.useRecoilValueFromAtom(sourceBankAccountId) - let countryCode = Utils.getCountryCode(country).isoAlpha2 - let stateCode = Utils.getStateCodeFromStateName(state.value, countryCode) - let giftCardNumber = Recoil.useRecoilValueFromAtom(userGiftCardNumber) - let giftCardPin = Recoil.useRecoilValueFromAtom(userGiftCardPin) - - let getFieldValueFromFieldType = (fieldType: PaymentMethodsRecord.paymentMethodsFields) => { - switch fieldType { - | Email => email.value - | AddressLine1 => line1.value - | AddressLine2 => line2.value - | AddressCity => city.value - | AddressPincode => postalCode.value - | AddressState => stateCode - | BlikCode => blikCode.value->Utils.removeHyphen - | PhoneNumber => phone.value - | PhoneCountryCode => phone.countryCode->Option.getOr("") - | Currency(_) => currency - | DocumentType(_) => documentType - | DocumentNumber => documentNumber.value - | Country => country - | LanguagePreference(languageOptions) => - languageOptions->Array.includes( - configValue.config.locale->String.toUpperCase->String.split("-")->Array.join("_"), - ) - ? configValue.config.locale - : "en" - | Bank => - ( - Bank.getBanks(paymentMethodType) - ->Array.find(item => item.displayName == selectedBank) - ->Option.getOr(Bank.defaultBank) - ).value - | AddressCountry(_) => { - let countryCode = - Country.getCountry(paymentMethodType, countryList) - ->Array.filter(item => item.countryName === country) - ->Array.get(0) - ->Option.getOr(Country.defaultTimeZone) - countryCode.isoAlpha2 - } - | BillingName => billingName.value - | CardNumber => cardNumber->CardValidations.clearSpaces - | GiftCardNumber => giftCardNumber.value - | GiftCardPin => giftCardPin.value - | CardExpiryMonth => - let (month, _) = CardUtils.getExpiryDates(cardExpiry) - month - | CardExpiryYear => - let (_, year) = CardUtils.getExpiryDates(cardExpiry) - year - | CryptoCurrencyNetworks => cryptoCurrencyNetworks - | DateOfBirth => - switch dateOfBirth->Nullable.toOption { - | Some(x) => x->Date.toISOString->String.slice(~start=0, ~end=10) - | None => "" - } - | CardCvc => cvcNumber - | VpaId => vpaId.value - | PixCNPJ => pixCNPJ.value - | PixCPF => pixCPF.value - | PixKey => pixKey.value - | IBAN - | BankAccountNumber => - bankAccountNumber.value - | SourceBankAccountId => sourceBankAccountId.value - | StateAndCity - | PhoneNumberAndCountryCode - | CountryAndPincode(_) - | SpecialField(_) - | InfoElement - | CardExpiryMonthAndYear - | CardExpiryAndCvc - | FullName - | ShippingName // Shipping Details are currently supported by only one click widgets - | ShippingAddressLine1 - | ShippingAddressLine2 - | ShippingAddressCity - | ShippingAddressPincode - | ShippingAddressState - | ShippingAddressCountry(_) - | BankList(_) - | None => "" - } - } - - let addBillingDetailsIfUseBillingAddress = requiredFieldsBody => { - if billingAddress.isUseBillingAddress { - billingAddressFields->Array.reduce(requiredFieldsBody, (acc, item) => { - let value = item->getFieldValueFromFieldType - if item === BillingName { - let arr = value->String.split(" ") - acc->Dict.set( - "payment_method_data.billing.address.first_name", - arr->Array.get(0)->Option.getOr("")->JSON.Encode.string, - ) - acc->Dict.set( - "payment_method_data.billing.address.last_name", - arr->Array.get(1)->Option.getOr("")->JSON.Encode.string, - ) - } else { - let path = item->getBillingAddressPathFromFieldType - acc->Dict.set(path, value->JSON.Encode.string) - } - acc - }) - } else { - requiredFieldsBody - } - } - - React.useEffect(() => { - let requiredFieldsBody = - requiredFields - ->Array.filter(item => item.field_type !== None) - ->Array.reduce(Dict.make(), (acc, item) => { - let value = switch item.field_type { - | BillingName => getName(item, billingName) - | FullName => getName(item, fullName) - | _ => item.field_type->getFieldValueFromFieldType - } - if value != "" { - if ( - isSavedCardFlow && - (item.field_type === BillingName || item.field_type === FullName) && - item.display_name === "card_holder_name" && - item.required_field === "payment_method_data.card.card_holder_name" - ) { - if !isAllStoredCardsHaveName { - acc->Dict.set( - "payment_method_data.card_token.card_holder_name", - value->JSON.Encode.string, - ) - } - } else { - acc->Dict.set(item.required_field, value->JSON.Encode.string) - } - } - acc - }) - ->addBillingDetailsIfUseBillingAddress - - setRequiredFieldsBody(_ => requiredFieldsBody) - None - }, ( - fullName.value, - email.value, - vpaId.value, - line1.value, - line2.value, - pixCNPJ.value, - pixCPF.value, - pixKey.value, - documentType, - documentNumber.value, - city.value, - postalCode.value, - state.value, - blikCode.value, - phone.value, - phone.countryCode, - currency, - billingName.value, - giftCardPin.value, - giftCardNumber.value, - country, - cardNumber, - cardExpiry, - cvcNumber, - selectedBank, - cryptoCurrencyNetworks, - dateOfBirth, - bankAccountNumber, - sourceBankAccountId, - )) -} +// let useSetInitialRequiredFields = ( +// ~requiredFields: array, +// ~paymentMethodType, +// ) => { +// let (email, setEmail) = Recoil.useRecoilState(userEmailAddress) +// let (fullName, setFullName) = Recoil.useRecoilState(userFullName) +// let (billingName, setBillingName) = Recoil.useRecoilState(userBillingName) +// let (line1, setLine1) = Recoil.useRecoilState(userAddressline1) +// let (line2, setLine2) = Recoil.useRecoilState(userAddressline2) +// let (phone, setPhone) = Recoil.useRecoilState(userPhoneNumber) +// let (state, setState) = Recoil.useRecoilState(userAddressState) +// let (city, setCity) = Recoil.useRecoilState(userAddressCity) +// let (postalCode, setPostalCode) = Recoil.useRecoilState(userAddressPincode) +// let (blikCode, setBlikCode) = Recoil.useRecoilState(userBlikCode) +// let (pixCNPJ, setPixCNPJ) = Recoil.useRecoilState(userPixCNPJ) +// let (pixCPF, setPixCPF) = Recoil.useRecoilState(userPixCPF) +// let (pixKey, setPixKey) = Recoil.useRecoilState(userPixKey) + +// let (country, setCountry) = Recoil.useRecoilState(userCountry) +// let (selectedBank, setSelectedBank) = Recoil.useRecoilState(userBank) +// let (currency, setCurrency) = Recoil.useRecoilState(userCurrency) +// let (documentType, setDocumentType) = Recoil.useRecoilState(userDocumentType) +// let (documentNumber, setDocumentNumber) = Recoil.useRecoilState(userDocumentNumber) +// let (cryptoCurrencyNetworks, setCryptoCurrencyNetworks) = Recoil.useRecoilState( +// cryptoCurrencyNetworks, +// ) +// let (dateOfBirth, setDateOfBirth) = Recoil.useRecoilState(dateOfBirth) +// let (bankAccountNumber, setBankAccountNumber) = Recoil.useRecoilState(userBankAccountNumber) +// let (sourceBankAccountId, setSourceBankAccountId) = Recoil.useRecoilState(sourceBankAccountId) +// let (giftCardNumber, setGiftCardNumber) = Recoil.useRecoilState(userGiftCardNumber) +// let (giftCardPin, setGiftCardPin) = Recoil.useRecoilState(userGiftCardPin) + +// React.useEffect(() => { +// let getNameValue = (item: PaymentMethodsRecord.required_fields) => { +// requiredFields +// ->Array.filter(requiredFields => requiredFields.field_type === item.field_type) +// ->Array.reduce("", (acc, item) => { +// let requiredFieldsArr = item.required_field->String.split(".") +// switch requiredFieldsArr->Array.get(requiredFieldsArr->Array.length - 1)->Option.getOr("") { +// | "first_name" => item.value->String.concat(acc) +// | "last_name" => acc->String.concatMany([" ", item.value]) +// | _ => acc +// } +// }) +// ->String.trim +// } + +// let setFields = ( +// setMethod: (RecoilAtomTypes.field => RecoilAtomTypes.field) => unit, +// field: RecoilAtomTypes.field, +// item: PaymentMethodsRecord.required_fields, +// isNameField, +// ~isCountryCodeAvailable=?, +// ) => { +// if isNameField && field.value === "" { +// setMethod(prev => { +// ...prev, +// value: getNameValue(item), +// }) +// if isCountryCodeAvailable->Option.isSome { +// setMethod(prev => { +// ...prev, +// countryCode: getNameValue(item), +// }) +// } +// } else if field.value === "" { +// if isCountryCodeAvailable->Option.isSome { +// setMethod(prev => { +// ...prev, +// countryCode: item.value, +// }) +// } else { +// setMethod(prev => { +// ...prev, +// value: item.value, +// }) +// } +// } +// } + +// requiredFields->Array.forEach(requiredField => { +// let value = requiredField.value +// switch requiredField.field_type { +// | Email => { +// let emailValue = email.value +// setFields(setEmail, email, requiredField, false) +// if emailValue === "" { +// let newEmail: RecoilAtomTypes.field = { +// value, +// isValid: None, +// errorString: "", +// } +// Utils.checkEmailValid(newEmail, setEmail) +// } +// } +// | FullName => setFields(setFullName, fullName, requiredField, true) +// | AddressLine1 => setFields(setLine1, line1, requiredField, false) +// | AddressLine2 => setFields(setLine2, line2, requiredField, false) +// | StateAndCity => { +// setFields(setState, state, requiredField, false) +// setFields(setCity, city, requiredField, false) +// } +// | CountryAndPincode(_) => { +// setFields(setPostalCode, postalCode, requiredField, false) +// if value !== "" && country === "" { +// let countryCode = +// Country.getCountry(paymentMethodType, countryList) +// ->Array.filter(item => item.isoAlpha2 === value) +// ->Array.get(0) +// ->Option.getOr(Country.defaultTimeZone) +// setCountry(_ => countryCode.countryName) +// } +// } +// | AddressState => setFields(setState, state, requiredField, false) +// | GiftCardNumber => setFields(setGiftCardNumber, giftCardNumber, requiredField, false) +// | GiftCardPin => setFields(setGiftCardPin, giftCardPin, requiredField, false) +// | AddressCity => setFields(setCity, city, requiredField, false) +// | PhoneCountryCode => +// setFields(setPhone, phone, requiredField, false, ~isCountryCodeAvailable=true) +// | AddressPincode => setFields(setPostalCode, postalCode, requiredField, false) +// | PhoneNumber => setFields(setPhone, phone, requiredField, false) +// | PhoneNumberAndCountryCode => +// setFields(setPhone, phone, requiredField, false, ~isCountryCodeAvailable=true) +// | BlikCode => setFields(setBlikCode, blikCode, requiredField, false) +// | PixKey => setFields(setPixKey, pixKey, requiredField, false) +// | PixCNPJ => setFields(setPixCNPJ, pixCNPJ, requiredField, false) +// | PixCPF => setFields(setPixCPF, pixCPF, requiredField, false) +// | BillingName => setFields(setBillingName, billingName, requiredField, true) +// | Country +// | AddressCountry(_) => +// if value !== "" { +// let defaultCountry = +// Country.getCountry(paymentMethodType, countryList) +// ->Array.filter(item => item.isoAlpha2 === value) +// ->Array.get(0) +// ->Option.getOr(Country.defaultTimeZone) +// setCountry(_ => defaultCountry.countryName) +// } +// | Currency(_) => +// if value !== "" && currency === "" { +// setCurrency(_ => value) +// } +// | Bank => +// if value !== "" && selectedBank === "" { +// setSelectedBank(_ => value) +// } +// | CryptoCurrencyNetworks => +// if value !== "" && cryptoCurrencyNetworks === "" { +// setCryptoCurrencyNetworks(_ => value) +// } +// | DateOfBirth => +// switch dateOfBirth->Nullable.toOption { +// | Some(x) => +// if value !== "" && x->Date.toDateString === "" { +// setDateOfBirth(_ => Nullable.make(x)) +// } +// | None => () +// } +// | IBAN +// | BankAccountNumber => +// setFields(setBankAccountNumber, bankAccountNumber, requiredField, false) +// | SourceBankAccountId => +// setFields(setSourceBankAccountId, sourceBankAccountId, requiredField, false) +// | DocumentType(_) => +// if value !== "" && documentType === "" { +// setDocumentType(_ => value) +// } +// | DocumentNumber => setFields(setDocumentNumber, documentNumber, requiredField, false) +// | LanguagePreference(_) +// | SpecialField(_) +// | InfoElement +// | CardNumber +// | CardExpiryMonth +// | CardExpiryYear +// | CardExpiryMonthAndYear +// | CardCvc +// | CardExpiryAndCvc +// | ShippingName // Shipping Details are currently supported by only one click widgets +// | ShippingAddressLine1 +// | ShippingAddressLine2 +// | ShippingAddressCity +// | ShippingAddressPincode +// | ShippingAddressState +// | ShippingAddressCountry(_) +// | BankList(_) +// | VpaId +// | None => () +// } +// }) +// None +// }, [requiredFields]) +// } + +// let useRequiredFieldsBody = ( +// ~requiredFields: array, +// ~paymentMethodType, +// ~cardNumber, +// ~cardExpiry, +// ~cvcNumber, +// ~isSavedCardFlow, +// ~isAllStoredCardsHaveName, +// ~setRequiredFieldsBody, +// ) => { +// let configValue = Recoil.useRecoilValueFromAtom(configAtom) +// let email = Recoil.useRecoilValueFromAtom(userEmailAddress) +// let vpaId = Recoil.useRecoilValueFromAtom(userVpaId) +// let pixCNPJ = Recoil.useRecoilValueFromAtom(userPixCNPJ) +// let pixCPF = Recoil.useRecoilValueFromAtom(userPixCPF) +// let pixKey = Recoil.useRecoilValueFromAtom(userPixKey) +// let fullName = Recoil.useRecoilValueFromAtom(userFullName) +// let billingName = Recoil.useRecoilValueFromAtom(userBillingName) +// let line1 = Recoil.useRecoilValueFromAtom(userAddressline1) +// let line2 = Recoil.useRecoilValueFromAtom(userAddressline2) +// let phone = Recoil.useRecoilValueFromAtom(userPhoneNumber) +// let state = Recoil.useRecoilValueFromAtom(userAddressState) +// let city = Recoil.useRecoilValueFromAtom(userAddressCity) +// let postalCode = Recoil.useRecoilValueFromAtom(userAddressPincode) +// let blikCode = Recoil.useRecoilValueFromAtom(userBlikCode) +// let country = Recoil.useRecoilValueFromAtom(userCountry) +// let selectedBank = Recoil.useRecoilValueFromAtom(userBank) +// let currency = Recoil.useRecoilValueFromAtom(userCurrency) +// let documentType = Recoil.useRecoilValueFromAtom(userDocumentType) +// let documentNumber = Recoil.useRecoilValueFromAtom(userDocumentNumber) +// let {billingAddress} = Recoil.useRecoilValueFromAtom(optionAtom) +// let cryptoCurrencyNetworks = Recoil.useRecoilValueFromAtom(cryptoCurrencyNetworks) +// let dateOfBirth = Recoil.useRecoilValueFromAtom(dateOfBirth) +// let bankAccountNumber = Recoil.useRecoilValueFromAtom(userBankAccountNumber) +// let sourceBankAccountId = Recoil.useRecoilValueFromAtom(sourceBankAccountId) +// let countryCode = Utils.getCountryCode(country).isoAlpha2 +// let stateCode = Utils.getStateCodeFromStateName(state.value, countryCode) +// let giftCardNumber = Recoil.useRecoilValueFromAtom(userGiftCardNumber) +// let giftCardPin = Recoil.useRecoilValueFromAtom(userGiftCardPin) + +// let getFieldValueFromFieldType = (fieldType: PaymentMethodsRecord.paymentMethodsFields) => { +// switch fieldType { +// | Email => email.value +// | AddressLine1 => line1.value +// | AddressLine2 => line2.value +// | AddressCity => city.value +// | AddressPincode => postalCode.value +// | AddressState => stateCode +// | BlikCode => blikCode.value->Utils.removeHyphen +// | PhoneNumber => phone.value +// | PhoneCountryCode => phone.countryCode->Option.getOr("") +// | Currency(_) => currency +// | DocumentType(_) => documentType +// | DocumentNumber => documentNumber.value +// | Country => country +// | LanguagePreference(languageOptions) => +// languageOptions->Array.includes( +// configValue.config.locale->String.toUpperCase->String.split("-")->Array.join("_"), +// ) +// ? configValue.config.locale +// : "en" +// | Bank => +// ( +// Bank.getBanks(paymentMethodType) +// ->Array.find(item => item.displayName == selectedBank) +// ->Option.getOr(Bank.defaultBank) +// ).value +// | AddressCountry(_) => { +// let countryCode = +// Country.getCountry(paymentMethodType, countryList) +// ->Array.filter(item => item.countryName === country) +// ->Array.get(0) +// ->Option.getOr(Country.defaultTimeZone) +// countryCode.isoAlpha2 +// } +// | BillingName => billingName.value +// | CardNumber => cardNumber->CardValidations.clearSpaces +// | GiftCardNumber => giftCardNumber.value +// | GiftCardPin => giftCardPin.value +// | CardExpiryMonth => +// let (month, _) = CardUtils.getExpiryDates(cardExpiry) +// month +// | CardExpiryYear => +// let (_, year) = CardUtils.getExpiryDates(cardExpiry) +// year +// | CryptoCurrencyNetworks => cryptoCurrencyNetworks +// | DateOfBirth => +// switch dateOfBirth->Nullable.toOption { +// | Some(x) => x->Date.toISOString->String.slice(~start=0, ~end=10) +// | None => "" +// } +// | CardCvc => cvcNumber +// | VpaId => vpaId.value +// | PixCNPJ => pixCNPJ.value +// | PixCPF => pixCPF.value +// | PixKey => pixKey.value +// | IBAN +// | BankAccountNumber => +// bankAccountNumber.value +// | SourceBankAccountId => sourceBankAccountId.value +// | StateAndCity +// | PhoneNumberAndCountryCode +// | CountryAndPincode(_) +// | SpecialField(_) +// | InfoElement +// | CardExpiryMonthAndYear +// | CardExpiryAndCvc +// | FullName +// | ShippingName // Shipping Details are currently supported by only one click widgets +// | ShippingAddressLine1 +// | ShippingAddressLine2 +// | ShippingAddressCity +// | ShippingAddressPincode +// | ShippingAddressState +// | ShippingAddressCountry(_) +// | BankList(_) +// | None => "" +// } +// } + +// let addBillingDetailsIfUseBillingAddress = requiredFieldsBody => { +// if billingAddress.isUseBillingAddress { +// billingAddressFields->Array.reduce(requiredFieldsBody, (acc, item) => { +// let value = item->getFieldValueFromFieldType +// if item === BillingName { +// let arr = value->String.split(" ") +// acc->Dict.set( +// "payment_method_data.billing.address.first_name", +// arr->Array.get(0)->Option.getOr("")->JSON.Encode.string, +// ) +// acc->Dict.set( +// "payment_method_data.billing.address.last_name", +// arr->Array.get(1)->Option.getOr("")->JSON.Encode.string, +// ) +// } else { +// let path = item->getBillingAddressPathFromFieldType +// acc->Dict.set(path, value->JSON.Encode.string) +// } +// acc +// }) +// } else { +// requiredFieldsBody +// } +// } + +// React.useEffect(() => { +// let requiredFieldsBody = +// requiredFields +// ->Array.filter(item => item.field_type !== None) +// ->Array.reduce(Dict.make(), (acc, item) => { +// let value = switch item.field_type { +// | BillingName => getName(item, billingName) +// | FullName => getName(item, fullName) +// | _ => item.field_type->getFieldValueFromFieldType +// } +// if value != "" { +// if ( +// isSavedCardFlow && +// (item.field_type === BillingName || item.field_type === FullName) && +// item.display_name === "card_holder_name" && +// item.required_field === "payment_method_data.card.card_holder_name" +// ) { +// if !isAllStoredCardsHaveName { +// acc->Dict.set( +// "payment_method_data.card_token.card_holder_name", +// value->JSON.Encode.string, +// ) +// } +// } else { +// acc->Dict.set(item.required_field, value->JSON.Encode.string) +// } +// } +// acc +// }) +// ->addBillingDetailsIfUseBillingAddress + +// setRequiredFieldsBody(_ => requiredFieldsBody) +// None +// }, ( +// fullName.value, +// email.value, +// vpaId.value, +// line1.value, +// line2.value, +// pixCNPJ.value, +// pixCPF.value, +// pixKey.value, +// documentType, +// documentNumber.value, +// city.value, +// postalCode.value, +// state.value, +// blikCode.value, +// phone.value, +// phone.countryCode, +// currency, +// billingName.value, +// giftCardPin.value, +// giftCardNumber.value, +// country, +// cardNumber, +// cardExpiry, +// cvcNumber, +// selectedBank, +// cryptoCurrencyNetworks, +// dateOfBirth, +// bankAccountNumber, +// sourceBankAccountId, +// )) +// } let isFieldTypeToRenderOutsideBilling = (fieldType: PaymentMethodsRecord.paymentMethodsFields) => { switch fieldType { @@ -958,7 +959,7 @@ let updateDynamicFields = ( ->combinePhoneNumberAndCountryCode } -let useSubmitCallback = () => { +let useSubmitCallback = (~onConfirm: option unit>=?) => { let (line1, setLine1) = Recoil.useRecoilState(userAddressline1) let (line2, setLine2) = Recoil.useRecoilState(userAddressline2) let (state, setState) = Recoil.useRecoilState(userAddressState) @@ -972,6 +973,13 @@ let useSubmitCallback = () => { let json = ev.data->Utils.safeParse let confirm = json->Utils.getDictFromJson->ConfirmType.itemToObjMapper if confirm.doSubmit { + // Trigger RFF submit so all fields are marked as touched and validation + // errors become visible on every field that has not been interacted with. + switch onConfirm { + | Some(submitFn) => submitFn() + | None => () + } + if line1.value == "" { setLine1(prev => { ...prev, @@ -1303,3 +1311,282 @@ let getGiftCardDataFromRequiredFieldsBody = requiredFieldsBody => { ->getDictFromDict("payment_method_data") data } + +// Helper function to map field types to validation rules +// Returns the validation rule directly - use with =? optional prop syntax +// For fields without validation, don't pass the validationRule prop at all +let getValidationRuleForFieldType = ( + fieldType: PaymentMethodsRecord.paymentMethodsFields, + ~country: string="", +): Validation.validationRule => { + switch fieldType { + | Email => Validation.Email + | FullName => Validation.FirstName + | BillingName => Validation.FirstName + | PhoneNumber => Validation.Phone + | AddressLine1 => Validation.Required + | AddressCity => Validation.Required + | AddressState => Validation.Required + | AddressPincode => Validation.PostalCode(country) + | BankAccountNumber => Validation.IBAN + | IBAN => Validation.IBAN + | VpaId => Validation.VpaId + | BlikCode => Validation.BlikCode + | PixKey => Validation.PixKey + | PixCPF => Validation.PixCPF + | PixCNPJ => Validation.PixCNPJ + | _ => Validation.Required // Default fallback + } +} + +// Check if a field type should have validation +// Returns false for optional fields like AddressLine2 +let shouldValidateFieldType = (fieldType: PaymentMethodsRecord.paymentMethodsFields): bool => { + switch fieldType { + | Email + | FullName + | BillingName + | PhoneNumber + | AddressLine1 + | AddressCity + | AddressState + | AddressPincode + | BankAccountNumber + | IBAN + | VpaId + | BlikCode + | PixKey + | PixCPF + | PixCNPJ + | DocumentNumber => true + | AddressLine2 => false // Optional field - no validation + | _ => false + } +} + +// Convert SuperpositionTypes.fieldType to PaymentMethodsRecord.paymentMethodsFields +// outputPath-first: the full path from superposition (e.g. "payment_method_data.card.card_cvc") +// is the most reliable signal. Fall back to fieldType only when no path matches. +let superpositionFieldTypeToPaymentMethodField = ( + fieldType: SuperpositionTypes.fieldType, + options: array, + outputPath: string, +): PaymentMethodsRecord.paymentMethodsFields => { + let p = outputPath->String.toLowerCase + + // --- Card fields (path-first to avoid PasswordInput/TextInput fallback mismatches) --- + if p->String.includes("card.card_cvc") || p->String.includes("card.cvc") { + CardCvc + } else if p->String.includes("card.card_number") || p->String.includes("card.number") { + CardNumber + } else if p->String.includes("card.card_exp_month") || p->String.includes("card.exp_month") { + CardExpiryMonth + } else if p->String.includes("card.card_exp_year") || p->String.includes("card.exp_year") { + CardExpiryYear + } else if ( + p->String.includes("billing.address.first_name") || + p->String.includes("billing.address.last_name") + ) { + FullName + } else if p->String.includes("billing.address.line1") { + AddressLine1 + } else if p->String.includes("billing.address.line2") { + AddressLine2 + } else if p->String.includes("billing.address.city") { + AddressCity + } else if p->String.includes("billing.address.state") { + AddressState + } else if p->String.includes("billing.address.country") { + AddressCountry(options) + } else if p->String.includes("billing.address.zip") { + AddressPincode + } else if p->String.includes("billing.email") { + Email + } else if p->String.includes("billing.phone.country_code") { + PhoneCountryCode + } else if p->String.includes("billing.phone") { + PhoneNumber + } else { + // --- Fallback to fieldType when no path match --- + switch fieldType { + | CardNumberTextInput => CardNumber + | CvcPasswordInput => CardCvc + | MonthSelect => CardExpiryMonth + | YearSelect => CardExpiryYear + | EmailInput => Email + | CountrySelect => AddressCountry(options) + | StateSelect => AddressState + | PhoneInput => PhoneNumber + | CountryCodeSelect => PhoneCountryCode + | CurrencySelect => Currency(options) + | DropdownSelect => BankList(options) + | DatePicker => DateOfBirth + | TextInput => FullName + | PasswordInput => FullName + } + } +} + +// Convert SuperpositionTypes.fieldConfig to PaymentMethodsRecord.required_fields +// fc.outputPath from superposition already includes the full path (e.g. "payment_method_data.card.card_cvc"), +// so we only prepend "payment_method_data." if the path doesn't already start with it. +let superpositionFieldToRequiredField = ( + fc: SuperpositionTypes.fieldConfig, + ~pmlRequiredFields: array, +): PaymentMethodsRecord.required_fields => { + let requiredFieldPath = + fc.outputPath->String.startsWith("payment_method_data.") + ? fc.outputPath + : "payment_method_data." ++ fc.outputPath + + let originalField = pmlRequiredFields->Array.find(r => r.required_field === requiredFieldPath) + let fieldType = switch originalField { + | Some(field) => field.field_type + | None => superpositionFieldTypeToPaymentMethodField(fc.fieldType, fc.options, fc.outputPath) + } + + { + required_field: requiredFieldPath, + display_name: fc.displayName, + field_type: fieldType, + value: "", + } +} + +// Get eligible connectors from payment method types (for cards from card_networks, for wallets from payment_experience) +let getEligibleConnectors = ( + paymentMethodTypes: PaymentMethodsRecord.paymentMethodTypes, + paymentMethod: string, +): array => { + if paymentMethod === "card" { + // For cards, get connectors from card_networks + paymentMethodTypes.card_networks + ->Array.flatMap(cn => cn.eligible_connectors) + ->Array.map(c => c->JSON.Encode.string) + } else { + // For other payment methods, get from payment_experience + paymentMethodTypes.payment_experience + ->Array.flatMap(pe => pe.eligible_connectors) + ->Array.map(c => c->JSON.Encode.string) + } +} + +// Build superpositionBaseContext from current payment state +let buildSuperpositionContext = ( + ~paymentMethod, + ~paymentMethodType, + ~userCountry, + ~paymentMethodListValue: PaymentMethodsRecord.paymentMethodList, +): SuperpositionTypes.superpositionBaseContext => { + let mandateType = switch paymentMethodListValue.payment_type { + | NEW_MANDATE => "new_mandate" + | SETUP_MANDATE => "setup_mandate" + | NORMAL => "non_mandate" + | NONE => "non_mandate" + } + + { + payment_method: paymentMethod, + payment_method_type: paymentMethodType, + country: userCountry, + mandate_type: mandateType, + collect_shipping_details_from_wallet_connector: "false", + collect_billing_details_from_wallet_connector: paymentMethodListValue.collect_billing_details_from_wallets + ? "true" + : "false", + } +} + +// Build requiredFieldsFromPML dict from PML required_fields array +// let buildRequiredFieldsFromPML = ( +// requiredFields: array, +// ): Dict.t => { +// requiredFields->Array.reduce(Dict.make(), (acc, field) => { +// let key = field.required_field // e.g. "payment_method_data.billing.address.city" +// // Strip "payment_method_data." prefix to get the outputPath superposition expects +// // let outputPath = key->String.replace("payment_method_data.", "") +// let outputPath = key +// if field.value !== "" { +// acc->Dict.set(outputPath, field.value) +// } +// acc +// }) +// } + +// Extract values from PML required_fields array for pre-filling +let extractValuesFromPMLRequiredFields = ( + requiredFields: array, +) => { + requiredFields->Array.reduce(Dict.make(), (acc, field) => { + if field.required_field !== "" { + acc->Dict.set(field.required_field, field.value) + } + acc + }) +} + +// Hook to get superposition fields - returns missingFields and initialValues +// PML values are passed to superposition to identify which fields are already pre-filled +// missingFields = fields that still need user input +// initialValues = pre-filled values from both PML AND superposition config +let useSuperpositionFields = ( + ~paymentMethod, + ~paymentMethodType, + ~paymentMethodTypes: PaymentMethodsRecord.paymentMethodTypes, + ~paymentMethodListValue: PaymentMethodsRecord.paymentMethodList, +) => { + let userCountry = Recoil.useRecoilValueFromAtom(userCountry) + let getSuperpositionFinalFields = ConfigurationService.useConfigurationServiceWeb() + + let (superpositionMissingFields, setSuperpositionMissingFields) = React.useState(_ => []) + let (initialValues, setInitialValues) = React.useState(_ => Dict.make()) + let (isLoading, setIsLoading) = React.useState(_ => false) + + React.useEffect(() => { + setSuperpositionMissingFields(_ => []) + setInitialValues(_ => Dict.make()) + setIsLoading(_ => true) + + let eligibleConnectors = getEligibleConnectors(paymentMethodTypes, paymentMethod) + + let configParams = buildSuperpositionContext( + ~paymentMethod, + ~paymentMethodType, + ~userCountry, + ~paymentMethodListValue, + ) + + // Pass PML values so superposition knows which fields are already pre-filled + // let pmlValues = buildRequiredFieldsFromPML(paymentMethodTypes.required_fields) + + let requiredFieldsFromPML = extractValuesFromPMLRequiredFields( + paymentMethodTypes.required_fields, + ) + + getSuperpositionFinalFields(eligibleConnectors, configParams, requiredFieldsFromPML) + ->Promise.then(((_requiredFields, missingRequiredFields, superpositionInitialValues)) => { + // Convert missing fields - these are fields that still need user input + let convertedMissingFields = + missingRequiredFields->Array.map( + fc => + superpositionFieldToRequiredField( + fc, + ~pmlRequiredFields=paymentMethodTypes.required_fields, + ), + ) + setSuperpositionMissingFields(_ => convertedMissingFields) + setInitialValues(_ => superpositionInitialValues) + setIsLoading(_ => false) + Promise.resolve() + }) + ->Promise.catch(ex => { + setIsLoading(_ => false) + Promise.resolve() + }) + ->ignore + + None + }, (paymentMethod, paymentMethodType, userCountry, paymentMethodTypes)) + + (superpositionMissingFields, initialValues, isLoading) +} diff --git a/src/Utilities/Utils.res b/src/Utilities/Utils.res index 5cc230937..01f82c507 100644 --- a/src/Utilities/Utils.res +++ b/src/Utilities/Utils.res @@ -1737,6 +1737,8 @@ let getFirstAndLastNameFromFullName = fullName => { (firstName, lastNameJson) } +let isEmptyDict = (dict: Dict.t<'a>) => dict->Dict.keysToArray->Array.length === 0 + let isKeyPresentInDict = (dict, key) => dict->Dict.get(key)->Option.isSome let minorUnitToString = val => (val->Int.toFloat /. 100.)->Float.toString