diff --git a/package-lock.json b/package-lock.json index aa758671b..71e13cf23 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 31698fea3..41f7acb84 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "react": "^18.2.0", "react-datepicker": "^8.4.0", "react-dom": "^18.2.0", + "react-final-form": "^7.0.0", "recoil": "^0.7.7", "webpack-merge": "^6.0.1" }, diff --git a/shared-code b/shared-code index 8a9c930c9..83c417ffc 160000 --- a/shared-code +++ b/shared-code @@ -1 +1 @@ -Subproject commit 8a9c930c9cca99cfcfccaa32e7a7d9cd4d56efb5 +Subproject commit 83c417ffca532218e15022ac3c337ac44fdcf92c diff --git a/src/Components/AddressPaymentInput.res b/src/Components/AddressPaymentInput.res index 753028e73..8cd4b2309 100644 --- a/src/Components/AddressPaymentInput.res +++ b/src/Components/AddressPaymentInput.res @@ -29,140 +29,101 @@ let showField = (val: PaymentType.addressType, type_: addressType) => { } @react.component -let make = (~className="", ~paymentType: option=?) => { +let make = ( + ~line1Path: string, + ~line2Path: string, + ~cityPath: string, + ~statePath: string, + ~countryPath: string, + ~postalPath: string, + ~className: string="", + ~paymentType: option=?, +) => { let {localeString, themeObj} = Recoil.useRecoilValueFromAtom(configAtom) let {fields} = Recoil.useRecoilValueFromAtom(optionAtom) let showDetails = getShowDetails(~billingDetails=fields.billingDetails) let contextPaymentType = usePaymentType() let paymentType = paymentType->Option.getOr(contextPaymentType) + let formState = ReactFinalForm.useFormState() + let submitFailed = formState.submitFailed - let (line1, setLine1) = Recoil.useRecoilState(userAddressline1) - let (line2, setLine2) = Recoil.useRecoilState(userAddressline2) - let (country, setCountry) = Recoil.useRecoilState(userAddressCountry) - let (city, setCity) = Recoil.useRecoilState(userAddressCity) - let (postalCode, setPostalCode) = Recoil.useRecoilState(userAddressPincode) - let (state, setState) = Recoil.useRecoilState(userAddressState) + let (showOtherFields, setShowOtherFields) = React.useState(_ => false) + + let countryData = CountryStateDataRefs.countryDataRef.contents + let countryNames = getCountryNames(countryData) + + let createValidator = rule => + Validation.createFieldValidator( + rule, + ~enabledCardSchemes=[], + ~localeObject=localeString->Obj.magic, + ) + + let line1Field = ReactFinalForm.useField( + line1Path, + ~config={validate: createValidator(Validation.Required)}, + ) + let line2Field = ReactFinalForm.useField(line2Path) + let cityField = ReactFinalForm.useField( + cityPath, + ~config={validate: createValidator(Validation.Required)}, + ) + let stateField = ReactFinalForm.useField( + statePath, + ~config={validate: createValidator(Validation.Required)}, + ) + let countryField = ReactFinalForm.useField(countryPath) + let postalField = ReactFinalForm.useField( + postalPath, + ~config={ + validate: createValidator(Validation.PostalCode(countryField.input.value->Option.getOr(""))), + }, + ) + + let stateNames = Utils.getStateNames({ + value: countryField.input.value->Option.getOr(""), + isValid: Some(true), + errorString: "", + }) let line1Ref = React.useRef(Nullable.null) let line2Ref = React.useRef(Nullable.null) let cityRef = React.useRef(Nullable.null) let postalRef = React.useRef(Nullable.null) - let (showOtherFileds, setShowOtherFields) = React.useState(_ => false) - - let stateNames = getStateNames(country) - let countryData = CountryStateDataRefs.countryDataRef.contents - let countryNames = getCountryNames(countryData) - - let checkPostalValidity = ( - postal: RecoilAtomTypes.field, - setPostal: (RecoilAtomTypes.field => RecoilAtomTypes.field) => unit, - ) => { - if postal.value !== "" { - setPostal(prev => { - ...prev, - isValid: Some(true), - errorString: "", - }) - } else { - setPostal(prev => { - ...prev, - isValid: Some(false), - errorString: localeString.postalCodeInvalidText, - }) - } - } - - let onPostalChange = ev => { - let val = ReactEvent.Form.target(ev)["value"] - setPostalCode(prev => { - ...prev, - value: val, - errorString: "", - }) - } + let hasDefaultValues = + line2Field.input.value->Option.getOr("") !== "" || + cityField.input.value->Option.getOr("") !== "" || + postalField.input.value->Option.getOr("") !== "" || + stateField.input.value->Option.getOr("") !== "" - let onPostalBlur = ev => { - let val = ReactEvent.Focus.target(ev)["value"] - if val !== "" { - setPostalCode(prev => { - ...prev, - isValid: Some(true), - errorString: "", - }) + let getErrorString = (field: ReactFinalForm.Field.fieldProps) => { + if (field.meta.touched && !field.meta.active) || submitFailed { + field.meta.error->Option.getOr("") } else { - setPostalCode(prev => { - ...prev, - isValid: Some(false), - errorString: localeString.postalCodeInvalidText, - }) + "" } } React.useEffect(() => { - checkPostalValidity(postalCode, setPostalCode) - setState(prev => { - ...prev, - value: "", - }) - + stateField.input.onChange("") None - }, [country.value]) - - let submitCallback = React.useCallback((ev: Window.event) => { - let json = ev.data->safeParse - let confirm = json->getDictFromJson->ConfirmType.itemToObjMapper - if confirm.doSubmit { - if line1.value == "" { - setLine1(prev => { - ...prev, - errorString: localeString.line1EmptyText, - }) - } - if line2.value == "" { - setLine2(prev => { - ...prev, - errorString: localeString.line2EmptyText, - }) - } - if state.value == "" { - setState(prev => { - ...prev, - errorString: localeString.stateEmptyText, - }) - } - if postalCode.value == "" { - setPostalCode(prev => { - ...prev, - errorString: localeString.postalCodeEmptyText, - }) - } - if city.value == "" { - setCity(prev => { - ...prev, - errorString: localeString.cityEmptyText, - }) - } - } - }, (line1, line2, country, state, city, postalCode)) - useSubmitPaymentData(submitCallback) - - let hasDefaulltValues = - line2.value !== "" || city.value !== "" || postalCode.value !== "" || state.value !== "" + }, [countryField.input.value])
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 +132,20 @@ let make = (~className="", ~paymentType: option=?) => { paymentType /> - +
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 +158,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 +202,16 @@ let make = (~className="", ~paymentType: option=?) => { 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 +222,15 @@ let make = (~className="", ~paymentType: option=?) => { Option.getOr(""), + isValid: Some(postalField.meta.valid), + errorString: getErrorString(postalField), + } + onBlur={_ev => postalField.input.onBlur()} + onChange={ev => { + postalField.input.onChange(ReactEvent.Form.target(ev)["value"]) + }} className name="postal" inputRef=postalRef diff --git a/src/Components/BillingNamePaymentInput.res b/src/Components/BillingNamePaymentInput.res index 3dc7796f4..0582017f7 100644 --- a/src/Components/BillingNamePaymentInput.res +++ b/src/Components/BillingNamePaymentInput.res @@ -1,67 +1,54 @@ 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.useField( + name, + ~config={validate: createValidator(Validation.Required)}, + ) + + let billingNameValue = field.input.value->Option.getOr("") + + let changeName = ev => { + let val = ReactEvent.Form.target(ev)["value"] + field.input.onChange(val) + } + + let onBlur = (_ev: JsxEventU.Focus.t) => { + field.input.onBlur() + } Option.getOr("") : "", + } onChange=changeName onBlur type_="text" diff --git a/src/Components/BlikCodePaymentInput.res b/src/Components/BlikCodePaymentInput.res index efc5a620b..566647503 100644 --- a/src/Components/BlikCodePaymentInput.res +++ b/src/Components/BlikCodePaymentInput.res @@ -1,11 +1,18 @@ 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 firstPart = formatted->String.slice(~start=0, ~end=3) @@ -20,45 +27,32 @@ let make = () => { } } + let field = 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() + } 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..9382da9fd 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.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 d600d63e8..71f99e34c 100644 --- a/src/Components/DynamicFields.res +++ b/src/Components/DynamicFields.res @@ -1,3 +1,5 @@ +open SuperpositionTypes + module DynamicFieldsToRenderWrapper = { @react.component let make = (~children, ~index, ~isInside=true) => { @@ -30,11 +32,13 @@ let make = ( ~isSaveDetailsWithClickToPay=false, ~isDisableInfoElement=false, ~isSplitPaymentsEnabled=false, + ~areCardFieldsRendered=false, ) => { open DynamicFieldsUtils open PaymentTypeContext open Utils open RecoilAtoms + let paymentMethodListValue = Recoil.useRecoilValueFromAtom(PaymentUtils.paymentMethodListValue) let paymentManagementListValue = Recoil.useRecoilValueFromAtom( PaymentUtils.paymentManagementListValue, @@ -61,109 +65,101 @@ let make = ( ~paymentMethodType, ) - let creditPaymentMethodTypes = PaymentUtils.usePaymentMethodTypeFromList( - ~paymentMethodListValue, - ~paymentMethod, - ~paymentMethodType="credit", - ) + // let creditPaymentMethodTypes = PaymentUtils.usePaymentMethodTypeFromList( + // ~paymentMethodListValue, + // ~paymentMethod, + // ~paymentMethodType="credit", + // ) - let creditPaymentMethodTypesV2 = PaymentUtilsV2.usePaymentMethodTypeFromListV2( - ~paymentsListValueV2=paymentManagementListValue, - ~paymentMethod, - ~paymentMethodType="credit", - ) + // let creditPaymentMethodTypesV2 = PaymentUtilsV2.usePaymentMethodTypeFromListV2( + // ~paymentsListValueV2=paymentManagementListValue, + // ~paymentMethod, + // ~paymentMethodType="credit", + // ) - let requiredFieldsWithBillingDetails = React.useMemo(() => { - if paymentMethod === "card" { - switch contextPaymentType { - | PaymentMethodsManagement => - let creditRequiredFields = - paymentManagementListValue.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 - - | _ => - let creditRequiredFields = creditPaymentMethodTypes.required_fields - - [ - ...paymentMethodTypes.required_fields, - ...creditRequiredFields, - ]->removeRequiredFieldsDuplicates - } - } else if dynamicFieldsEnabledPaymentMethods->Array.includes(paymentMethodType) { - switch contextPaymentType { - | PaymentMethodsManagement => paymentMethodTypesV2.requiredFields - | _ => paymentMethodTypes.required_fields - } - } else { - [] - } - }, ( - paymentMethod, - paymentMethodTypes.required_fields, - paymentMethodTypesV2.requiredFields, - paymentMethodType, - creditPaymentMethodTypes.required_fields, - creditPaymentMethodTypesV2.requiredFields, - )) - - let requiredFields = React.useMemo(() => { - requiredFieldsWithBillingDetails - ->removeBillingDetailsIfUseBillingAddress(billingAddress) - ->removeClickToPayFieldsIfSaveDetailsWithClickToPay(isSaveDetailsWithClickToPay) - }, (requiredFieldsWithBillingDetails, isSaveDetailsWithClickToPay)) - - let isAllStoredCardsHaveName = React.useMemo(() => { - PaymentType.getIsStoredPaymentMethodHasName(savedMethod) - }, [savedMethod]) + // let requiredFieldsWithBillingDetails = React.useMemo(() => { + // if paymentMethod === "card" { + // switch GlobalVars.sdkVersion { + // | V2 => + // let creditRequiredFields = + // paymentManagementListValue.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 - let clickToPayConfig = Recoil.useRecoilValueFromAtom(RecoilAtoms.clickToPayConfig) + // | V1 => + // let creditRequiredFields = creditPaymentMethodTypes.required_fields - let fieldsArr = React.useMemo(() => { - PaymentMethodsRecord.getPaymentMethodFields( - paymentMethodType, - requiredFields, - ~isSavedCardFlow, - ~isAllStoredCardsHaveName, - ~localeString, - ) - ->updateDynamicFields(billingAddress, isSaveDetailsWithClickToPay, clickToPayConfig) - ->Belt.SortArray.stableSortBy(PaymentMethodsRecord.sortPaymentMethodFields) - //<...>// - }, (requiredFields, isAllStoredCardsHaveName, isSavedCardFlow, isSaveDetailsWithClickToPay)) + // [ + // ...paymentMethodTypes.required_fields, + // ...creditRequiredFields, + // ]->removeRequiredFieldsDuplicates + // } + // } else if dynamicFieldsEnabledPaymentMethods->Array.includes(paymentMethodType) { + // switch GlobalVars.sdkVersion { + // | V1 => paymentMethodTypes.required_fields + // | V2 => paymentMethodTypesV2.requiredFields + // } + // } else { + // [] + // } + // }, ( + // paymentMethod, + // paymentMethodTypes.required_fields, + // paymentMethodTypesV2.requiredFields, + // paymentMethodType, + // creditPaymentMethodTypes.required_fields, + // creditPaymentMethodTypesV2.requiredFields, + // )) - let isSpacedInnerLayout = config.appearance.innerLayout === Spaced + // let requiredFields = React.useMemo(() => { + // requiredFieldsWithBillingDetails + // ->removeBillingDetailsIfUseBillingAddress(billingAddress) + // ->removeClickToPayFieldsIfSaveDetailsWithClickToPay(isSaveDetailsWithClickToPay) + // }, (requiredFieldsWithBillingDetails, isSaveDetailsWithClickToPay)) - let (line1, setLine1) = Recoil.useRecoilState(userAddressline1) - let (line2, setLine2) = Recoil.useRecoilState(userAddressline2) - let (city, setCity) = Recoil.useRecoilState(userAddressCity) - let (state, setState) = Recoil.useRecoilState(userAddressState) - let (postalCode, setPostalCode) = Recoil.useRecoilState(userAddressPincode) + // let isAllStoredCardsHaveName = React.useMemo(() => { + // PaymentType.getIsStoredPaymentMethodHasName(savedMethod) + // }, [savedMethod]) + + let (missingRequiredFields, initialValues, _) = useSuperpositionFields( + ~paymentMethod, + ~paymentMethodType, + ~paymentMethodTypes, + ~paymentMethodListValue, + ) + + let processedFieldConfigs = React.useMemo(() => { + missingRequiredFields + ->removeBillingDetailsFromFieldConfigs(billingAddress) + ->removeClickToPayFieldsFromFieldConfigs(isSaveDetailsWithClickToPay) + ->removeCardFieldsFromFieldConfigs(areCardFieldsRendered) + ->removeCardNetworkFromFieldConfigs + ->processFieldConfigs(billingAddress, isSaveDetailsWithClickToPay) + }, (missingRequiredFields, billingAddress, isSaveDetailsWithClickToPay, areCardFieldsRendered)) + + // let clickToPayConfig = Recoil.useRecoilValueFromAtom(RecoilAtoms.clickToPayConfig) + + let isSpacedInnerLayout = config.appearance.innerLayout === Spaced let (currency, setCurrency) = Recoil.useRecoilState(userCurrency) let line1Ref = React.useRef(Nullable.null) let line2Ref = React.useRef(Nullable.null) let cityRef = React.useRef(Nullable.null) + let stateRef = React.useRef(Nullable.null) let bankAccountNumberRef = React.useRef(Nullable.null) let sourceBankAccountIdRef = React.useRef(Nullable.null) let postalRef = React.useRef(Nullable.null) let (selectedBank, setSelectedBank) = Recoil.useRecoilState(userBank) let (country, setCountry) = Recoil.useRecoilState(userCountry) - - let (bankAccountNumber, setBankAccountNumber) = Recoil.useRecoilState(userBankAccountNumber) - let (sourceBankAccountId, setSourceBankAccountId) = Recoil.useRecoilState(sourceBankAccountId) - let countryList = CountryStateDataRefs.countryDataRef.contents let stateNames = getStateNames({ value: country, isValid: None, @@ -174,6 +170,13 @@ let make = ( let countryNames = getCountryNames(Country.getCountry(paymentMethodType, countryList)) + let initialCountryIso = { + let effectiveCountry = country !== "" ? country : countryNames->Array.get(0)->Option.getOr("") + Utils.getCountryCode(effectiveCountry).isoAlpha2 + } + + let countryIso = Utils.getCountryCode(country).isoAlpha2 + let setCurrency = val => { setCurrency(val) } @@ -248,613 +251,924 @@ let make = ( None }) - let onPostalChange = ev => { - let val = ReactEvent.Form.target(ev)["value"] - - if val !== "" { - setPostalCode(_ => { - isValid: Some(true), - value: val, - errorString: "", - }) - } else { - setPostalCode(_ => { - isValid: Some(false), - value: val, - errorString: "", - }) + let formSubmitRef = React.useRef(None) + + let submitCallback = useSubmitCallback(~onConfirm=() => { + switch formSubmitRef.current { + | Some(submitFn) => submitFn() + | None => () } - } + }) + useSubmitPaymentData(submitCallback) - useRequiredFieldsEmptyAndValid( - ~requiredFields, - ~fieldsArr, - ~countryNames, - ~bankNames, - ~isCardValid, - ~isExpiryValid, - ~isCVCValid, - ~cardNumber, - ~cardExpiry, - ~cvcNumber, - ~isSavedCardFlow, - ) + let bottomElement = - useSetInitialRequiredFields( - ~requiredFields={ - billingAddress.usePrefilledValues === Auto ? requiredFieldsWithBillingDetails : requiredFields - }, - ~paymentMethodType, - ) + let fullNameConfig = React.useMemo(() => { + missingRequiredFields + ->Array.find(fc => + switch fc.fieldType { + | FullNameInput(_) => true + | _ => false + } + ) + ->Option.map(fc => + switch fc.fieldType { + | FullNameInput(config) => config + | _ => {firstName: None, lastName: None} + } + ) + ->Option.getOr({firstName: None, lastName: None}) + }, [missingRequiredFields]) - useRequiredFieldsBody( - ~requiredFields, - ~paymentMethodType, - ~cardNumber, - ~cardExpiry, - ~cvcNumber, - ~isSavedCardFlow, - ~isAllStoredCardsHaveName, - ~setRequiredFieldsBody, - ) + let firstNamePath = React.useMemo(() => { + fullNameConfig.firstName->Option.map(fc => fc.outputPath)->Option.getOr("") + }, [fullNameConfig]) - let submitCallback = useSubmitCallback() - useSubmitPaymentData(submitCallback) + let lastNamePath = React.useMemo(() => { + fullNameConfig.lastName->Option.map(fc => fc.outputPath)->Option.getOr("") + }, [fullNameConfig]) - let bottomElement = + // State + City: both present → render as a side-by-side pair. + let cityOutputPath = React.useMemo(() => { + processedFieldConfigs->DynamicFieldsUtils.getOutputPathForFieldType(AddressCityInput) + }, [processedFieldConfigs]) - let getCustomFieldName = (item: PaymentMethodsRecord.paymentMethodsFields) => { - if ( - requiredFields - ->Array.filter(requiredFieldType => - requiredFieldType.field_type === item && - requiredFieldType.display_name === "card_holder_name" - ) - ->Array.length > 0 - ) { - Some(localeString.cardHolderName) - } else { - None - } - } + let stateOutputPath = React.useMemo(() => { + processedFieldConfigs->DynamicFieldsUtils.getOutputPathForFieldType(AddressStateInput) + }, [processedFieldConfigs]) + + let hasBothStateAndCity = React.useMemo(() => { + processedFieldConfigs->DynamicFieldsUtils.hasBothFieldTypes(AddressCityInput, AddressStateInput) + }, [processedFieldConfigs]) + + // Country + Postal: both present → render as a side-by-side pair. + let countryOutputPath = React.useMemo(() => { + processedFieldConfigs->DynamicFieldsUtils.getOutputPathForFieldType(AddressCountryInput) + }, [processedFieldConfigs]) + + let postalOutputPath = React.useMemo(() => { + processedFieldConfigs->DynamicFieldsUtils.getOutputPathForFieldType(AddressPostalCodeInput) + }, [processedFieldConfigs]) + + let hasBothCountryAndPostal = React.useMemo(() => { + processedFieldConfigs->DynamicFieldsUtils.hasBothFieldTypes( + AddressCountryInput, + AddressPostalCodeInput, + ) + }, [processedFieldConfigs]) + + // Phone + CountryCode: both present → render as a combined phone input. + let phoneNumberOutputPath = React.useMemo(() => { + processedFieldConfigs->DynamicFieldsUtils.getOutputPathForFieldType(PhoneInput) + }, [processedFieldConfigs]) + + let countryCodeOutputPath = React.useMemo(() => { + processedFieldConfigs->DynamicFieldsUtils.getOutputPathForFieldType(CountryCodeSelect) + }, [processedFieldConfigs]) + + let hasBothPhoneAndCountryCode = React.useMemo(() => { + processedFieldConfigs->DynamicFieldsUtils.hasBothFieldTypes(PhoneInput, CountryCodeSelect) + }, [processedFieldConfigs]) + + // Month + Year expiry: both present → render as a single expiry input (outside billing). + let hasBothMonthAndYear = React.useMemo(() => { + processedFieldConfigs->DynamicFieldsUtils.hasBothFieldTypes(MonthSelect, YearSelect) + }, [processedFieldConfigs]) - let dynamicFieldsToRenderOutsideBilling = React.useMemo(() => { - fieldsArr->Array.filter(isFieldTypeToRenderOutsideBilling) - }, [fieldsArr]) + // CvcPasswordInput alongside month+year: + // render expiry + CVC side-by-side. + let hasExpiryAndCvc = React.useMemo(() => { + hasBothMonthAndYear && processedFieldConfigs->DynamicFieldsUtils.hasFieldType(CvcPasswordInput) + }, [processedFieldConfigs]) - let dynamicFieldsToRenderInsideBilling = React.useMemo(() => { - fieldsArr->Array.filter(field => !(field->isFieldTypeToRenderOutsideBilling)) - }, [fieldsArr]) + // Split fields into outside and inside billing sections + let fieldsOutsideBilling = React.useMemo(() => { + processedFieldConfigs->Array.filter(fc => fc->isFieldTypeToRenderOutsideBillingConfig) + }, [processedFieldConfigs]) - let isInfoElementPresent = dynamicFieldsToRenderOutsideBilling->Array.includes(InfoElement) + let fieldsInsideBilling = React.useMemo(() => { + processedFieldConfigs->Array.filter(fc => !(fc->isFieldTypeToRenderOutsideBillingConfig)) + }, [processedFieldConfigs]) + + let isInfoElementPresent = + fieldsOutsideBilling + ->Array.find(fc => fc.fieldType == InfoElementType) + ->Option.isSome let isRenderInfoElement = isInfoElementPresent && !isDisableInfoElement - let isRenderDynamicFieldsInsideBilling = dynamicFieldsToRenderInsideBilling->Array.length > 0 + let isRenderDynamicFieldsInsideBilling = fieldsInsideBilling->Array.length > 0 let spacedStylesForBiilingDetails = isSpacedInnerLayout ? "p-2" : "my-2" - Array.length > 0}> - {<> - {dynamicFieldsToRenderOutsideBilling - ->Array.mapWithIndex((item, index) => { - Int.toString} index={index} isInside={false}> - {switch item { - | CardNumber => - - | GiftCardNumber => - | CardExpiryMonth - | CardExpiryYear - | CardExpiryMonthAndYear => - - | CardCvc => - - | GiftCardPin => - - | CardExpiryAndCvc => -
- - -
- | Currency(currencyArr) => - let updatedCurrencyArray = - currencyArr->DropdownField.updateArrayOfStringToOptionsTypeArray - - | DocumentType(opt) => { - let updatedDocumentTypeArray = - opt->DropdownField.updateArrayOfStringToOptionsTypeArrayWithUpperCaseLabel - - } - | FullName => - <> - -
- {item->getCustomFieldName->Option.getOr("")->React.string} -
-
- getCustomFieldName} - optionalRequiredFields={Some(requiredFields)} - /> - - | CryptoCurrencyNetworks => - | DateOfBirth => - | VpaId => - | PixKey => - | PixCPF => - | PixCNPJ => - | BankAccountNumber | IBAN => - { - let value = ReactEvent.Form.target(ev)["value"] - setBankAccountNumber(_ => { - isValid: Some(value !== ""), - value, - errorString: value !== "" ? "" : localeString.ibanEmptyText, - }) - }} - onBlur={ev => { - let value = ReactEvent.Focus.target(ev)["value"] - setBankAccountNumber(prev => { - ...prev, - errorString: value !== "" ? "" : localeString.ibanEmptyText, - isValid: Some(value !== ""), - }) - }} - type_="text" - name="bankAccountNumber" - maxLength=42 - inputRef=bankAccountNumberRef - placeholder="DE00 0000 0000 0000 0000 00" - /> - | SourceBankAccountId => - { - let value = ReactEvent.Form.target(ev)["value"] - setSourceBankAccountId(_ => { - isValid: Some(value !== ""), - value, - errorString: value !== "" ? "" : localeString.sourceBankAccountIdEmptyText, - }) - }} - onBlur={ev => { - let value = ReactEvent.Focus.target(ev)["value"] - setSourceBankAccountId(prev => { - ...prev, - isValid: Some(value !== ""), - }) - }} - type_="text" - name="sourceBankAccountId" - maxLength=42 - inputRef=sourceBankAccountIdRef - placeholder="DE00 0000 0000 0000 0000 00" - /> - | DocumentNumber - | Email - | InfoElement - | Country - | Bank - | None - | BillingName - | PhoneNumber - | AddressLine1 - | AddressLine2 - | AddressCity - | StateAndCity - | AddressPincode - | AddressState - | BlikCode - | SpecialField(_) - | CountryAndPincode(_) - | AddressCountry(_) - | ShippingName // Shipping Details are currently supported by only one click widgets - | ShippingAddressLine1 - | ShippingAddressLine2 - | ShippingAddressCity - | ShippingAddressPincode - | ShippingAddressState - | PhoneCountryCode - | PhoneNumberAndCountryCode - | LanguagePreference(_) - | ShippingAddressCountry(_) - | BankList(_) => React.null - }} -
- }) - ->React.array} - -
-
- {React.string(localeString.billingDetailsText)} -
-
- {dynamicFieldsToRenderInsideBilling - ->Array.mapWithIndex((item, index) => { - Int.toString} index={index}> - {switch item { - | BillingName => - | Email => - | PhoneNumberAndCountryCode => - | StateAndCity => -
- { - let value = ReactEvent.Form.target(ev)["value"] - setCity(prev => { - isValid: Some(value !== ""), - value, - errorString: value !== "" ? "" : prev.errorString, - }) - }} - onBlur={ev => { - let value = ReactEvent.Focus.target(ev)["value"] - setCity(prev => { - ...prev, - isValid: Some(value !== ""), - }) - }} - type_="text" - name="city" - inputRef=cityRef - placeholder=localeString.cityLabel - className={isSpacedInnerLayout ? "" : "!border-r-0"} + let formValidator = React.useMemo(() => { + _ => Dict.make() + }, [processedFieldConfigs]) + + let (_, setAreRequiredFieldsValid) = Recoil.useRecoilState(areRequiredFieldsValid) + + Array.length > 0}> + ()} + initialValues={Some(initialValues)} + validate={Some(formValidator)} + render={formProps => { + formSubmitRef.current = Some(formProps.form.submit) + let submitFailed = formProps.submitFailed + ReactFinalForm.useFormStateHandler( + ~onFormChange=values => { + // Flatten the nested form values so keys align correctly during merge using `mergeAndFlattenToTuples`. + let flatValues = values->JSON.Encode.object->Utils.flattenObject(false) + setRequiredFieldsBody(_ => flatValues) + }, + ~onValidationChange=isValid => { + setAreRequiredFieldsValid(_ => isValid) + }, + ~formProps, + ) + <> + {fieldsOutsideBilling + ->Array.mapWithIndex((item, index) => { + Int.toString} index={index} isInside={false}> + {switch item.fieldType { + | CardNumberTextInput => + + | GiftCardNumberInput => + | GiftCardPinInput => + | MonthSelect => + if hasExpiryAndCvc { +
+ + - Array.length > 0}> - -
- | CountryAndPincode(countryArr) => - let updatedCountryArray = - countryArr->DropdownField.updateArrayOfStringToOptionsTypeArray -
+ } else { + + } + | YearSelect => React.null + | CvcPasswordInput => + if hasExpiryAndCvc { + React.null + } else { + + } + | CurrencySelect => + let updatedCurrencyArray = + item.options->DropdownField.updateArrayOfStringToOptionsTypeArray + { + let val = field.input.value->Option.getOr(currency) { + let newVal = setter(val) + setCurrency(_ => newVal) + field.input.onChange(newVal) + }} disabled=false - options=updatedCountryArray - className={isSpacedInnerLayout ? "" : "!border-t-0 !border-r-0"} + options=updatedCurrencyArray /> + }} + /> + | DocumentTypeSelect => { + let updatedDocumentTypeArray = + item.options->DropdownField.updateArrayOfStringToOptionsTypeArrayWithUpperCaseLabel + + } + | FullNameInput(_) => + let defaultName = + paymentMethod === "card" + ? localeString.cardHolderName + : localeString.fullNameLabel + <> + +
+ {defaultName->React.string} +
+
+ + + | CryptoNetworkSelect => + | DatePicker => + | VpaTextInput => + | PixKeyInput => + | PixCpfInput => + | PixCnpjInput => + | BankAccountNumberInput => + { + let val = field.input.value->Option.getOr("") { - let value = ReactEvent.Focus.target(ev)["value"] - setPostalCode(prev => { - ...prev, - isValid: Some(value !== ""), - }) + fieldName="Account Number" + value={ + RecoilAtomTypes.value: val, + isValid: Some(field.meta.valid), + errorString: submitFailed || field.meta.touched + ? field.meta.error->Option.getOr("") + : "", + } + onChange={ev => { + let rawValue = ReactEvent.Form.target(ev)["value"] + let cleanValue = rawValue->Validation.clearSpaces + field.input.onChange(cleanValue) }} - onChange=onPostalChange - name="postal" - inputRef=postalRef - placeholder=localeString.postalCodeLabel - className={isSpacedInnerLayout ? "" : "!border-t-0"} + onBlur={_ev => field.input.onBlur()} + type_="tel" + name="bankAccountNumber" + maxLength=17 + inputRef=bankAccountNumberRef + placeholder="000123456789" /> -
- | AddressLine1 => - { - let value = ReactEvent.Form.target(ev)["value"] - setLine1(prev => { - isValid: Some(value !== ""), - value, - errorString: value !== "" ? "" : prev.errorString, - }) - }} - onBlur={ev => { - let value = ReactEvent.Focus.target(ev)["value"] - setLine1(prev => { - ...prev, - isValid: Some(value !== ""), - }) - }} - type_="text" - name="line1" - inputRef=line1Ref - placeholder=localeString.line1Placeholder - className={isSpacedInnerLayout ? "" : "!border-b-0"} - /> - | AddressLine2 => - { - let value = ReactEvent.Form.target(ev)["value"] - setLine2(prev => { - isValid: Some(value !== ""), - value, - errorString: value !== "" ? "" : prev.errorString, - }) - }} - onBlur={ev => { - let value = ReactEvent.Focus.target(ev)["value"] - setLine2(prev => { - ...prev, - isValid: Some(value !== ""), - }) - }} - type_="text" - name="line2" - inputRef=line2Ref - placeholder=localeString.line2Placeholder - /> - | AddressCity => - { - let value = ReactEvent.Form.target(ev)["value"] - setCity(prev => { - isValid: Some(value !== ""), - value, - errorString: value !== "" ? "" : prev.errorString, - }) - }} - onBlur={ev => { - let value = ReactEvent.Focus.target(ev)["value"] - setCity(prev => { - ...prev, - isValid: Some(value !== ""), - }) - }} - type_="text" - name="city" - inputRef=cityRef - placeholder=localeString.cityLabel - /> - | AddressState => - Array.length > 0}> - + | IbanInput => + { + let val = field.input.value->Option.getOr("") + Option.getOr("") + : "", + } + onChange={ev => field.input.onChange(ReactEvent.Form.target(ev)["value"])} + onBlur={_ev => field.input.onBlur()} + type_="text" + name="iban" + maxLength=42 + inputRef={React.useRef(Nullable.null)} + placeholder="DE00 0000 0000 0000 0000 00" /> - - | AddressPincode => - { - let value = ReactEvent.Focus.target(ev)["value"] - setPostalCode(prev => { - ...prev, - isValid: Some(value !== ""), - }) + }} + /> + | SourceBankAccountIdInput => + { + let val = field.input.value->Option.getOr("") + Option.getOr("") + : "", + } + onChange={ev => field.input.onChange(ReactEvent.Form.target(ev)["value"])} + onBlur={_ev => field.input.onBlur()} + type_="text" + name="sourceBankAccountId" + maxLength=42 + inputRef=sourceBankAccountIdRef + placeholder="DE00 0000 0000 0000 0000 00" + /> + }} + /> + | DocumentNumberInput + | EmailInput + | InfoElementType + | CountrySelect + | BankSelect + | BankListSelect + | PhoneInput + | AddressLine1Input + | AddressLine2Input + | AddressCityInput + | AddressPostalCodeInput + | AddressStateInput + | BlikCodeInput + | AddressCountryInput + | // | ShippingNameInput // Shipping Details are currently supported by only one click widgets + // | ShippingAddressLine1Input + // | ShippingAddressLine2Input + // | ShippingAddressCityInput + // | ShippingAddressPostalCodeInput + // | ShippingAddressStateInput + // | ShippingAddressCountryInput + CountryCodeSelect + | TextInput + | PasswordInput + | StateSelect + | DropdownSelect => React.null + }} +
+ }) + ->React.array} + +
+
+ {React.string(localeString.billingDetailsText)} +
+
+ {fieldsInsideBilling + ->Array.mapWithIndex((item, index) => { + Int.toString} index={index}> + {switch item.fieldType { + | EmailInput => + { + let val = field.input.value->Option.getOr("") + Option.getOr("") + : "", + } + onChange={ev => + field.input.onChange(ReactEvent.Form.target(ev)["value"])} + onBlur={_ev => field.input.onBlur()} + type_="email" + name=TestUtils.emailInputTestId + inputRef={React.useRef(Nullable.null)} + placeholder="Eg: johndoe@gmail.com" + /> + }} + /> + | PhoneInput => + if hasBothPhoneAndCountryCode { + + } else { + { + let val = field.input.value->Option.getOr("") + Option.getOr("") + : "", + } + onChange={ev => + field.input.onChange(ReactEvent.Form.target(ev)["value"])} + onBlur={_ev => field.input.onBlur()} + type_="tel" + name="phone" + inputRef={React.useRef(Nullable.null)} + placeholder="000 000 000" + /> + }} + /> + } + | CountryCodeSelect => React.null + | AddressCityInput => + if hasBothStateAndCity { +
+ { + let val = field.input.value->Option.getOr("") + Option.getOr("") + : "", + } + onChange={ev => + field.input.onChange(ReactEvent.Form.target(ev)["value"])} + onBlur={_ev => field.input.onBlur()} + type_="text" + name="city" + inputRef=cityRef + placeholder=localeString.cityLabel + className={isSpacedInnerLayout ? "" : "!border-r-0"} + /> + }} + /> + { + let val = field.input.value->Option.getOr("") + if stateNames->Array.length > 0 { + Option.getOr("") + : "", + } + setValue={setter => { + let newVal = setter({ + value: val, + isValid: Some(field.meta.valid), + errorString: "", + }) + let countryIso = Utils.getCountryCode(country).isoAlpha2 + let stateCode = Utils.getStateCodeFromStateName( + newVal.value, + countryIso, + ) + field.input.onChange(stateCode) + }} + options={stateNames} + /> + } else { + Option.getOr("") + : "", + } + onChange={ev => + field.input.onChange(ReactEvent.Form.target(ev)["value"])} + onBlur={_ev => field.input.onBlur()} + type_="text" + name="state" + placeholder=localeString.stateLabel + inputRef=stateRef + /> + } + }} + /> +
+ } else { + { + let val = field.input.value->Option.getOr("") + Option.getOr("") + : "", + } + onChange={ev => + field.input.onChange(ReactEvent.Form.target(ev)["value"])} + onBlur={_ev => field.input.onBlur()} + type_="text" + name="city" + inputRef=cityRef + placeholder=localeString.cityLabel + /> + }} + /> + } + | AddressStateInput => + if hasBothStateAndCity { + React.null + } else { + { + let val = field.input.value->Option.getOr("") + if stateNames->Array.length > 0 { + Option.getOr("") + : "", + } + setValue={setter => { + let newVal = setter({ + value: val, + isValid: Some(field.meta.valid), + errorString: "", + }) + let countryIso = Utils.getCountryCode(country).isoAlpha2 + let stateCode = Utils.getStateCodeFromStateName( + newVal.value, + countryIso, + ) + field.input.onChange(stateCode) + }} + options={stateNames} + /> + } else { + Option.getOr("") + : "", + } + onChange={ev => + field.input.onChange(ReactEvent.Form.target(ev)["value"])} + onBlur={_ev => field.input.onBlur()} + type_="text" + name="state" + placeholder=localeString.stateLabel + inputRef=stateRef + /> + } + }} + /> + } + | AddressCountryInput => + if hasBothCountryAndPostal { + let updatedCountryArray = + countryNames->DropdownField.updateArrayOfStringToOptionsTypeArray +
+ { + { + let newVal = setter(field.input.value->Option.getOr(country)) + setCountry(_ => newVal) + let countryIso = Utils.getCountryCode(newVal).isoAlpha2 + field.input.onChange(countryIso) + }} + disabled=false + options=updatedCountryArray + className={isSpacedInnerLayout ? "" : "!border-t-0 !border-r-0"} + /> + }} + /> + { + let val = field.input.value->Option.getOr("") + Option.getOr("") + : "", + } + onChange={ev => + field.input.onChange(ReactEvent.Form.target(ev)["value"])} + onBlur={_ev => field.input.onBlur()} + name="postal" + inputRef=postalRef + placeholder=localeString.postalCodeLabel + className={isSpacedInnerLayout ? "" : "!border-t-0"} + /> + }} + /> +
+ } else { + let updatedCountryArr = + countryNames->DropdownField.updateArrayOfStringToOptionsTypeArray + { + { + let newVal = setter(field.input.value->Option.getOr(country)) + setCountry(_ => newVal) + let countryIso = Utils.getCountryCode(newVal).isoAlpha2 + field.input.onChange(countryIso) + }} + disabled=false + options=updatedCountryArr + /> + }} + /> + } + | AddressPostalCodeInput => + if hasBothCountryAndPostal { + React.null + } else { + { + let val = field.input.value->Option.getOr("") + Option.getOr("") + : "", + } + onChange={ev => + field.input.onChange(ReactEvent.Form.target(ev)["value"])} + onBlur={_ev => field.input.onBlur()} + name="postal" + inputRef=postalRef + placeholder=localeString.postalCodeLabel + /> + }} + /> + } + | AddressLine1Input => + { + let val = field.input.value->Option.getOr("") + Option.getOr("") + : "", + } + onChange={ev => + field.input.onChange(ReactEvent.Form.target(ev)["value"])} + onBlur={_ev => field.input.onBlur()} + type_="text" + name="line1" + inputRef=line1Ref + placeholder=localeString.line1Placeholder + className={isSpacedInnerLayout ? "" : "!border-b-0"} + /> + }} + /> + | AddressLine2Input => + { + let val = field.input.value->Option.getOr("") + Option.getOr("") + : "", + } + onChange={ev => + field.input.onChange(ReactEvent.Form.target(ev)["value"])} + onBlur={_ev => field.input.onBlur()} + type_="text" + name="line2" + inputRef=line2Ref + placeholder=localeString.line2Placeholder + /> + }} + /> + | BlikCodeInput => + | CountrySelect => + let updatedCountryNames = + countryNames->DropdownField.updateArrayOfStringToOptionsTypeArray + { + { + let newVal = setter(field.input.value->Option.getOr(country)) + setCountry(_ => newVal) + let countryIso = Utils.getCountryCode(newVal).isoAlpha2 + field.input.onChange(countryIso) + }} + disabled=false + options=updatedCountryNames + /> + }} + /> + | BankListSelect => + let updatedBankNames = + Bank.getBanks(paymentMethodType) + ->getBankNames(item.options) + ->DropdownField.updateArrayOfStringToOptionsTypeArray + { + let val = field.input.value->Option.getOr(selectedBank) + { + let newVal = setter(val) + setSelectedBank(_ => newVal) + field.input.onChange(newVal) + }} + disabled=false + options=updatedBankNames + /> + }} + /> + | BankSelect => + let updatedBankNames = + bankNames->DropdownField.updateArrayOfStringToOptionsTypeArray + { + let val = field.input.value->Option.getOr(selectedBank) + { + let newVal = setter(val) + setSelectedBank(_ => newVal) + field.input.onChange(newVal) + }} + disabled=false + options=updatedBankNames + /> + }} + /> + | InfoElementType + | PixKeyInput + | PixCpfInput + | PixCnpjInput + | DocumentTypeSelect + | DocumentNumberInput + | CardNumberTextInput + | MonthSelect + | YearSelect + | CvcPasswordInput + | CurrencySelect + | FullNameInput(_) + | GiftCardNumberInput + | GiftCardPinInput + | // | ShippingNameInput // Shipping Details are currently supported by only one click widgets + // | ShippingAddressLine1Input + // | ShippingAddressLine2Input + // | ShippingAddressCityInput + // | ShippingAddressPostalCodeInput + // | ShippingAddressStateInput + // | ShippingAddressCountryInput + CryptoNetworkSelect + | DatePicker + | VpaTextInput + | BankAccountNumberInput + | IbanInput + | SourceBankAccountIdInput + | TextInput + | PasswordInput + | StateSelect + | DropdownSelect => React.null }} - onChange=onPostalChange - name="postal" - inputRef=postalRef - placeholder=localeString.postalCodeLabel - /> - | BlikCode => - | Country => - let updatedCountryNames = - countryNames->DropdownField.updateArrayOfStringToOptionsTypeArray - - | AddressCountry(countryArr) => - let updatedCountryArr = - countryArr->DropdownField.updateArrayOfStringToOptionsTypeArray - - | BankList(bankArr) => - let updatedBankNames = - Bank.getBanks(paymentMethodType) - ->getBankNames(bankArr) - ->DropdownField.updateArrayOfStringToOptionsTypeArray - - | Bank => - let updatedBankNames = - bankNames->DropdownField.updateArrayOfStringToOptionsTypeArray - - | SpecialField(element) => element - | InfoElement - | PixKey - | PixCPF - | PixCNPJ - | DocumentType(_) - | DocumentNumber - | CardNumber - | CardExpiryMonth - | CardExpiryYear - | CardExpiryMonthAndYear - | CardCvc - | CardExpiryAndCvc - | Currency(_) - | FullName - | GiftCardNumber - | GiftCardPin - | ShippingName // Shipping Details are currently supported by only one click widgets - | ShippingAddressLine1 - | ShippingAddressLine2 - | ShippingAddressCity - | ShippingAddressPincode - | ShippingAddressState - | ShippingAddressCountry(_) - | CryptoCurrencyNetworks - | DateOfBirth - | PhoneNumber - | PhoneCountryCode - | VpaId - | LanguagePreference(_) - | BankAccountNumber - | IBAN - | SourceBankAccountId - | None => React.null - }} -
- }) - ->React.array} -
-
-
- - - {<> - {if fieldsArr->Array.length > 1 { - bottomElement - } else { - - }} - } - - } + + }) + ->React.array} +
+
+ + + + {<> + {if processedFieldConfigs->Array.length > 1 { + bottomElement + } else { + + }} + } + + + }} + /> } diff --git a/src/Components/EmailPaymentInput.res b/src/Components/EmailPaymentInput.res index 05b4c5708..e31371fbc 100644 --- a/src/Components/EmailPaymentInput.res +++ b/src/Components/EmailPaymentInput.res @@ -1,63 +1,46 @@ 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.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() + } Option.getOr("") : "", + } onChange=changeEmail onBlur type_="email" diff --git a/src/Components/FullNamePaymentInput.res b/src/Components/FullNamePaymentInput.res index 5b539179e..8486d9b52 100644 --- a/src/Components/FullNamePaymentInput.res +++ b/src/Components/FullNamePaymentInput.res @@ -1,71 +1,89 @@ open RecoilAtoms open PaymentType -open Utils @react.component -let make = (~customFieldName=None, ~optionalRequiredFields=None) => { +let make = (~customFieldName, ~firstNamePath, ~lastNamePath) => { let {localeString} = Recoil.useRecoilValueFromAtom(configAtom) let {fields} = Recoil.useRecoilValueFromAtom(optionAtom) - let (fullName, setFullName) = Recoil.useRecoilState(userFullName) - let showDetails = getShowDetails(~billingDetails=fields.billingDetails) - let changeName = ev => { - let val: string = ReactEvent.Form.target(ev)["value"] - setFullName(prev => validateName(val, prev, localeString)) + let (placeholder, fieldName) = switch customFieldName { + | Some(val) => (val, val) + | None => (localeString.fullNamePlaceholder, localeString.fullNameLabel) } - let onBlur = ev => { - let val: string = ReactEvent.Focus.target(ev)["value"] - setFullName(prev => validateName(val, prev, localeString)) + let createValidator = rule => + Validation.createFieldValidator( + rule, + ~enabledCardSchemes=[], + ~localeObject=localeString->Obj.magic, + ) + + let showDetails = getShowDetails(~billingDetails=fields.billingDetails) + + let firstField = ReactFinalForm.useField( + firstNamePath, + ~config={ + validate: createValidator(Validation.FirstName), + }, + ) + let lastField = ReactFinalForm.useField( + lastNamePath, + ~config={ + validate: createValidator(Validation.LastName), + }, + ) + + // Local state: the combined display value shown in the single . + let (inputValue, setInputValue) = React.useState(() => "") + + let handleChange = ev => { + let value: string = ReactEvent.Form.target(ev)["value"] + setInputValue(_ => value) + let spaceIndex = value->String.indexOf(" ") + if spaceIndex === -1 { + firstField.input.onChange(value) + lastField.input.onChange("") + } else { + let firstName = value->String.substring(~start=0, ~end=spaceIndex) + let lastName = value->String.substringToEnd(~start=spaceIndex + 1) + firstField.input.onChange(firstName) + lastField.input.onChange(lastName) + } } - let (placeholder, fieldName) = switch customFieldName { - | Some(val) => (val, val) - | None => (localeString.fullNamePlaceholder, localeString.fullNameLabel) + let onBlur = (_ev: JsxEventU.Focus.t) => { + firstField.input.onBlur() + lastField.input.onBlur() } - let nameRef = React.useRef(Nullable.null) - React.useEffect(() => { - setFullName(prev => validateName(prev.value, prev, localeString)) - None - }, []) + let nameRef = React.useRef(Nullable.null) - let submitCallback = React.useCallback((ev: Window.event) => { - let json = ev.data->safeParse - let confirm = json->getDictFromJson->ConfirmType.itemToObjMapper - if confirm.doSubmit { - if fullName.value == "" { - setFullName(prev => { - ...prev, - errorString: fieldName->localeString.nameEmptyText, - }) - } else if !(fullName.isValid->Option.getOr(false)) { - setFullName(prev => { - ...prev, - errorString: localeString.invalidCardHolderNameError, - }) - } else { - switch optionalRequiredFields { - | Some(requiredFields) => - if !DynamicFieldsUtils.checkIfNameIsValid(requiredFields, FullName, fullName) { - setFullName(prev => { - ...prev, - errorString: fieldName->localeString.completeNameEmptyText, - }) - } - | None => () - } - } + let errorString = if ( + (firstField.meta.touched && !firstField.meta.active) || + (lastField.meta.touched && !lastField.meta.active) + ) { + switch (firstField.meta.error, lastField.meta.error) { + | (Some(err), _) => err + | (_, Some(err)) => err + | _ => "" } - }, [fullName]) - useSubmitPaymentData(submitCallback) + } else { + "" + } + + let isValid = + firstField.meta.valid && + (lastField.meta.valid || !lastField.meta.touched || lastField.meta.active) { +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.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) - 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..7ad00eb2e 100644 --- a/src/Components/GiftCardPinInput.res +++ b/src/Components/GiftCardPinInput.res @@ -1,51 +1,42 @@ @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.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) - Option.getOr("") : "", + } onChange=changeGiftCardPin - onBlur=onBlurGiftCardPin + onBlur type_="text" name="giftCardPin" inputRef=giftCardPinRef diff --git a/src/Components/ManageSavedItem.res b/src/Components/ManageSavedItem.res index 7a784c0f8..f5f0ec976 100644 --- a/src/Components/ManageSavedItem.res +++ b/src/Components/ManageSavedItem.res @@ -1,4 +1,5 @@ open RecoilAtoms +open DynamicFieldsUtils @react.component let make = ( @@ -24,6 +25,49 @@ let make = ( | _ => "" } + let paymentMethodListValue = Recoil.useRecoilValueFromAtom(PaymentUtils.paymentMethodListValue) + + let paymentMethod = "card" + let paymentMethodType = "credit" + + let paymentMethodTypes = PaymentUtils.usePaymentMethodTypeFromList( + ~paymentMethodListValue, + ~paymentMethod, + ~paymentMethodType, + ) + + let (superpositionMissingFields, initialValues, _) = useSuperpositionFields( + ~paymentMethod, + ~paymentMethodType, + ~paymentMethodTypes, + ~paymentMethodListValue, + ) + + let fullNameConfig = React.useMemo(() => { + superpositionMissingFields + ->Array.find(fc => + switch fc.fieldType { + | FullNameInput(_) => true + | _ => false + } + ) + ->Option.map(fc => + switch fc.fieldType { + | FullNameInput(config) => config + | _ => {firstName: None, lastName: None} + } + ) + ->Option.getOr({firstName: None, lastName: None}) + }, [superpositionMissingFields]) + + let firstNamePath = React.useMemo(() => { + fullNameConfig.firstName->Option.map(fc => fc.outputPath)->Option.getOr("") + }, [fullNameConfig]) + + let lastNamePath = React.useMemo(() => { + fullNameConfig.lastName->Option.map(fc => fc.outputPath)->Option.getOr("") + }, [fullNameConfig]) + React.useEffect(() => { startTransition(() => { setFullName(prev => Utils.validateName(cardHolderName, prev, localeString)) @@ -32,6 +76,10 @@ let make = ( None }, []) + let formValidator = React.useMemo(() => { + _ => Dict.make() + }, []) +
- - + ()} + initialValues={Some(initialValues)} + validate={Some(formValidator)} + render={_ => { + <> + + + + }} + />
} diff --git a/src/Components/NicknamePaymentInput.res b/src/Components/NicknamePaymentInput.res index c376e7d53..d3518bf1b 100644 --- a/src/Components/NicknamePaymentInput.res +++ b/src/Components/NicknamePaymentInput.res @@ -1,25 +1,39 @@ @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.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)) + let val = ReactEvent.Form.target(ev)["value"] + 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("") : "", + } onChange onBlur type_="userCardNickName" diff --git a/src/Components/PaymentField.res b/src/Components/PaymentField.res index 7047796c4..53d7e5481 100644 --- a/src/Components/PaymentField.res +++ b/src/Components/PaymentField.res @@ -4,7 +4,6 @@ open PaymentTypeContext @react.component let make = ( - ~setValue=?, ~value: RecoilAtomTypes.field, ~valueDropDown=?, ~setValueDropDown=?, @@ -38,15 +37,6 @@ let make = ( let handleFocus = _ => { setInputFocused(_ => true) - switch setValue { - | Some(fn) => - fn(prev => { - ...prev, - isValid: None, - errorString: "", - }) - | None => () - } Utils.handleOnFocusPostMessage(~targetOrigin=parentURL) } diff --git a/src/Components/PhoneNumberPaymentInput.res b/src/Components/PhoneNumberPaymentInput.res index 13aa49776..92f029834 100644 --- a/src/Components/PhoneNumberPaymentInput.res +++ b/src/Components/PhoneNumberPaymentInput.res @@ -1,13 +1,30 @@ @react.component -let make = () => { +let make = (~numberName: string, ~codeName: string) => { open RecoilAtoms open PaymentType open Utils + let {localeString} = Recoil.useRecoilValueFromAtom(configAtom) + + let createValidator = rule => + Validation.createFieldValidator( + rule, + ~enabledCardSchemes=[], + ~localeObject=localeString->Obj.magic, + ) + + let numberField = ReactFinalForm.useField( + numberName, + ~config={validate: createValidator(Validation.Phone)}, + ) + let codeField = ReactFinalForm.useField(codeName) + + let numberVal = numberField.input.value->Option.getOr("") + let codeVal = codeField.input.value->Option.getOr("") + let phoneRef = React.useRef(Nullable.null) let {fields} = Recoil.useRecoilValueFromAtom(optionAtom) let showDetails = getShowDetails(~billingDetails=fields.billingDetails) - let (phone, setPhone) = Recoil.useRecoilState(userPhoneNumber) let clientTimeZone = CardUtils.dateTimeFormat().resolvedOptions().timeZone let clientCountry = getClientCountry(clientTimeZone) let currentCountryCode = Utils.getCountryCode(clientCountry.countryName) @@ -54,19 +71,14 @@ let make = () => { let getCountryCodeSplitValue = val => val->String.split("#")->Array.get(1)->Option.getOr("") let changePhone = ev => { - let val: string = ReactEvent.Form.target(ev)["value"]->String.replaceRegExp(%re("/\D|\s/g"), "") - setPhone(prev => { - ...prev, - countryCode: valueDropDown->getCountryCodeSplitValue, - value: val, - }) + let val: string = + ReactEvent.Form.target(ev)["value"]->String.replaceRegExp(%re("/\\D|\\s/g"), "") + numberField.input.onChange(val) + codeField.input.onChange(valueDropDown->getCountryCodeSplitValue) } React.useEffect(() => { - setPhone(prev => { - ...prev, - countryCode: valueDropDown->getCountryCodeSplitValue, - }) + codeField.input.onChange(valueDropDown->getCountryCodeSplitValue) None }, [valueDropDown]) @@ -86,7 +98,12 @@ let make = () => { Option.getOr("") : "", + } onChange=changePhone paymentType=Payment type_="tel" diff --git a/src/Components/PixPaymentInput.res b/src/Components/PixPaymentInput.res index 17512eee9..567281ac6 100644 --- a/src/Components/PixPaymentInput.res +++ b/src/Components/PixPaymentInput.res @@ -1,88 +1,40 @@ @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 = CnpjValidation.isValidCNPJ(val) - 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 = CpfValidation.isValidCPF(val) - 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.useField( + name, + ~config={validate: createValidator(validationRule)}, + ) + + let pixValue = field.input.value->Option.getOr("") let onChange = ev => { let val = ReactEvent.Form.target(ev)["value"] @@ -93,51 +45,20 @@ let make = (~fieldType="") => { | "pixCPF" => val->CardValidations.clearSpaces | _ => val } - validateAndSetPixInputValue(transformedVal) + field.input.onChange(transformedVal) } - 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) - Option.getOr("") : "", + } onChange onBlur type_=fieldType diff --git a/src/Components/ReactFinalFormField.res b/src/Components/ReactFinalFormField.res new file mode 100644 index 000000000..7e39cbf64 --- /dev/null +++ b/src/Components/ReactFinalFormField.res @@ -0,0 +1,26 @@ +@react.component +let make = (~name: string, ~validationRule=?, ~initialValue="", ~render) => { + let {localeString} = Recoil.useRecoilValueFromAtom(RecoilAtoms.configAtom) + + let createValidator = rule => + Validation.createFieldValidator( + rule, + ~enabledCardSchemes=[], + ~localeObject=localeString->Obj.magic, + ) + + let hasInitialValue = initialValue !== "" + + let field = switch (validationRule, hasInitialValue) { + | (Some(rule), true) => + ReactFinalForm.useField( + name, + ~config={validate: createValidator(rule), initialValue: Some(initialValue)}, + ) + | (Some(rule), false) => ReactFinalForm.useField(name, ~config={validate: createValidator(rule)}) + | (None, true) => ReactFinalForm.useField(name, ~config={initialValue: Some(initialValue)}) + | (None, false) => ReactFinalForm.useField(name) + } + + render(field) +} diff --git a/src/Components/VpaIdPaymentInput.res b/src/Components/VpaIdPaymentInput.res index c3fe5df03..801ae25dd 100644 --- a/src/Components/VpaIdPaymentInput.res +++ b/src/Components/VpaIdPaymentInput.res @@ -2,53 +2,40 @@ 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.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) - Option.getOr("") : "", + } onChange=changeVpaId onBlur type_="text" diff --git a/src/LocaleStrings/ArabicLocale.res b/src/LocaleStrings/ArabicLocale.res index 4098b9015..b3dfdd494 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}`, @@ -260,5 +261,6 @@ let localeStrings: LocaleStringTypes.localeStrings = { installmentSelectPlanPlaceholder: "اختر خطة التقسيط", showMore: "عرض المزيد", showLess: "عرض أقل", + mandatoryFieldText: "هذا الحقل إلزامي.", refreshingText: "جارٍ التحديث...", } diff --git a/src/LocaleStrings/CatalanLocale.res b/src/LocaleStrings/CatalanLocale.res index e530923c9..222dc17d2 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`, @@ -259,5 +260,6 @@ let localeStrings: LocaleStringTypes.localeStrings = { installmentSelectPlanPlaceholder: "Seleccioneu un pla de terminis", showMore: "Mostra més", showLess: "Mostra menys", + mandatoryFieldText: "Aquest camp és obligatori", refreshingText: "Actualitzant...", } diff --git a/src/LocaleStrings/ChineseLocale.res b/src/LocaleStrings/ChineseLocale.res index afabf5caa..aa7ecb15e 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}`, @@ -257,5 +258,6 @@ let localeStrings: LocaleStringTypes.localeStrings = { installmentSelectPlanPlaceholder: "选择分期计划", showMore: "显示更多", showLess: "收起", + mandatoryFieldText: "此字段为必填项", refreshingText: "刷新中...", } diff --git a/src/LocaleStrings/DeutschLocale.res b/src/LocaleStrings/DeutschLocale.res index 44a1ceec0..d3dbdbfa3 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}`, @@ -258,5 +259,6 @@ let localeStrings: LocaleStringTypes.localeStrings = { installmentSelectPlanPlaceholder: "Ratenplan auswählen", showMore: "Mehr anzeigen", showLess: "Weniger anzeigen", + mandatoryFieldText: "Dieses Feld ist obligatorisch", refreshingText: "Aktualisierung...", } diff --git a/src/LocaleStrings/DutchLocale.res b/src/LocaleStrings/DutchLocale.res index 0a83d36ea..51240368a 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`, @@ -257,5 +258,6 @@ let localeStrings: LocaleStringTypes.localeStrings = { installmentSelectPlanPlaceholder: "Selecteer een aflossingsplan", showMore: "Meer tonen", showLess: "Minder tonen", + mandatoryFieldText: "This field is mandatory", refreshingText: "Vernieuwen...", } diff --git a/src/LocaleStrings/EnglishGBLocale.res b/src/LocaleStrings/EnglishGBLocale.res index b42e85e0f..9ec4dd54a 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}`, @@ -257,5 +258,6 @@ let localeStrings: LocaleStringTypes.localeStrings = { installmentSelectPlanPlaceholder: "Select instalment plan", showMore: "Show more", showLess: "Show less", + mandatoryFieldText: "This field is required", refreshingText: "Refreshing...", } diff --git a/src/LocaleStrings/EnglishLocale.res b/src/LocaleStrings/EnglishLocale.res index 49f98931a..a600fecf5 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}`, @@ -257,5 +258,6 @@ let localeStrings: LocaleStringTypes.localeStrings = { installmentSelectPlanPlaceholder: "Select installment plan", showMore: "Show more", showLess: "Show less", + mandatoryFieldText: "This field is mandatory", refreshingText: "Refreshing...", } diff --git a/src/LocaleStrings/FrenchBelgiumLocale.res b/src/LocaleStrings/FrenchBelgiumLocale.res index e198f791a..c3de9cb0f 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`, @@ -259,5 +260,6 @@ let localeStrings: LocaleStringTypes.localeStrings = { installmentSelectPlanPlaceholder: "Sélectionner un plan de paiement", showMore: "Afficher plus", showLess: "Afficher moins", + mandatoryFieldText: "Ce champ est obligatoire", refreshingText: "Actualisation...", } diff --git a/src/LocaleStrings/FrenchLocale.res b/src/LocaleStrings/FrenchLocale.res index 805dca3bd..1ed775a2a 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}`, @@ -259,5 +260,6 @@ let localeStrings: LocaleStringTypes.localeStrings = { installmentSelectPlanPlaceholder: "Sélectionner un plan de paiement", showMore: "Afficher plus", showLess: "Afficher moins", + mandatoryFieldText: "Ce champ est obligatoire", refreshingText: "Actualisation...", } diff --git a/src/LocaleStrings/HebrewLocale.res b/src/LocaleStrings/HebrewLocale.res index df7bde257..25e3f930f 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}`, @@ -258,5 +259,6 @@ let localeStrings: LocaleStringTypes.localeStrings = { installmentSelectPlanPlaceholder: "בחר תוכנית תשלומים", showMore: "הצג עוד", showLess: "הצג פחות", + mandatoryFieldText: "שדה זה הוא חובה", refreshingText: "מרענן...", } diff --git a/src/LocaleStrings/ItalianLocale.res b/src/LocaleStrings/ItalianLocale.res index 8afad0365..7f44eabe3 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`, @@ -259,5 +260,6 @@ let localeStrings: LocaleStringTypes.localeStrings = { installmentSelectPlanPlaceholder: "Seleziona un piano a rate", showMore: "Mostra di più", showLess: "Mostra meno", + mandatoryFieldText: "Questo campo è obbligatorio", refreshingText: "Aggiornamento...", } diff --git a/src/LocaleStrings/JapaneseLocale.res b/src/LocaleStrings/JapaneseLocale.res index 642889bc0..14c608953 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}`, @@ -258,5 +259,6 @@ let localeStrings: LocaleStringTypes.localeStrings = { installmentSelectPlanPlaceholder: "分割払いプランを選択", showMore: "もっと見る", showLess: "表示を減らす", + mandatoryFieldText: "このフィールドは必須です", refreshingText: "更新中...", } diff --git a/src/LocaleStrings/LocaleStringTypes.res b/src/LocaleStrings/LocaleStringTypes.res index 2422d9f3b..7d2f06dcb 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, @@ -245,6 +246,7 @@ type localeStrings = { installmentSelectPlanPlaceholder: string, showMore: string, showLess: string, + mandatoryFieldText: string, refreshingText: string, } diff --git a/src/LocaleStrings/PolishLocale.res b/src/LocaleStrings/PolishLocale.res index 7843a19e3..8218227b1 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`, @@ -258,5 +259,6 @@ let localeStrings: LocaleStringTypes.localeStrings = { installmentSelectPlanPlaceholder: "Wybierz plan rat", showMore: "Pokaż więcej", showLess: "Pokaż mniej", + mandatoryFieldText: "To pole jest obowiązkowe", refreshingText: "Odświeżanie...", } diff --git a/src/LocaleStrings/PortugueseLocale.res b/src/LocaleStrings/PortugueseLocale.res index 19e5a8dd9..d8980600c 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`, @@ -258,5 +259,6 @@ let localeStrings: LocaleStringTypes.localeStrings = { installmentSelectPlanPlaceholder: "Selecione um plano de parcelamento", showMore: "Mostrar mais", showLess: "Mostrar menos", + mandatoryFieldText: "Este campo é obrigatório", refreshingText: "Atualizando...", } diff --git a/src/LocaleStrings/RussianLocale.res b/src/LocaleStrings/RussianLocale.res index 483557b0f..ab8094cd0 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 не может быть пустым`, @@ -266,5 +267,6 @@ let localeStrings: LocaleStringTypes.localeStrings = { installmentSelectPlanPlaceholder: "Выберите план рассрочки", showMore: "Показать ещё", showLess: "Показать меньше", + mandatoryFieldText: "Это поле обязательно для заполнения.", refreshingText: "Обновление...", } diff --git a/src/LocaleStrings/SpanishLocale.res b/src/LocaleStrings/SpanishLocale.res index e53164efd..fc857acfe 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`, @@ -258,5 +259,6 @@ let localeStrings: LocaleStringTypes.localeStrings = { installmentSelectPlanPlaceholder: "Seleccione un plan de cuotas", showMore: "Mostrar más", showLess: "Mostrar menos", + mandatoryFieldText: "Este campo es obligatorio", refreshingText: "Actualizando...", } diff --git a/src/LocaleStrings/SwedishLocale.res b/src/LocaleStrings/SwedishLocale.res index bba9dad94..0af5fc1d1 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`, @@ -257,5 +258,6 @@ let localeStrings: LocaleStringTypes.localeStrings = { installmentSelectPlanPlaceholder: "Välj ett avbetalningsplan", showMore: "Visa mer", showLess: "Visa mindre", + mandatoryFieldText: "Detta fält är obligatoriskt", refreshingText: "Uppdaterar...", } diff --git a/src/LocaleStrings/TraditionalChineseLocale.res b/src/LocaleStrings/TraditionalChineseLocale.res index 0b346d6b3..2f958816c 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}`, @@ -257,5 +258,6 @@ let localeStrings: LocaleStringTypes.localeStrings = { installmentSelectPlanPlaceholder: "選擇分期計劃", showMore: "顯示更多", showLess: "收起", + mandatoryFieldText: "此欄位為必填項", refreshingText: "重新整理中...", } diff --git a/src/Payments/ACHBankDebit.res b/src/Payments/ACHBankDebit.res index bc14d311d..84279ad46 100644 --- a/src/Payments/ACHBankDebit.res +++ b/src/Payments/ACHBankDebit.res @@ -1,18 +1,17 @@ open RecoilAtoms open Utils -open PaymentModeType +open ACHTypes @react.component let make = () => { let {themeObj} = Recoil.useRecoilValueFromAtom(configAtom) let {displaySavedPaymentMethods} = Recoil.useRecoilValueFromAtom(optionAtom) let isManualRetryEnabled = Recoil.useRecoilValueFromAtom(isManualRetryEnabled) + let areRequiredFieldsValid = Recoil.useRecoilValueFromAtom(areRequiredFieldsValid) + let areRequiredFieldsEmpty = Recoil.useRecoilValueFromAtom(areRequiredFieldsEmpty) let loggerState = Recoil.useRecoilValueFromAtom(loggerAtom) - let email = Recoil.useRecoilValueFromAtom(userEmailAddress) - let fullName = Recoil.useRecoilValueFromAtom(userFullName) - let intent = PaymentHelpers.usePaymentIntent(Some(loggerState), BankDebits) let (bankError, setBankError) = React.useState(_ => "") @@ -22,14 +21,7 @@ let make = () => { let (modalData, setModalData) = React.useState(_ => None) let toolTipRef = React.useRef(Nullable.null) - let line1 = Recoil.useRecoilValueFromAtom(userAddressline1) - let line2 = Recoil.useRecoilValueFromAtom(userAddressline2) - let country = Recoil.useRecoilValueFromAtom(userAddressCountry) - let city = Recoil.useRecoilValueFromAtom(userAddressCity) - let postalCode = Recoil.useRecoilValueFromAtom(userAddressPincode) - let state = Recoil.useRecoilValueFromAtom(userAddressState) - let countryCode = Utils.getCountryCode(country.value).isoAlpha2 - let stateCode = Utils.getStateCodeFromStateName(state.value, countryCode) + let paymentMethodListValue = Recoil.useRecoilValueFromAtom(PaymentUtils.paymentMethodListValue) let pmAuthMapper = React.useMemo1( @@ -56,12 +48,10 @@ let make = () => { None }, [modalData]) - let complete = - email.value != "" && - fullName.value != "" && - email.isValid->Option.getOr(false) && - modalData->Option.isSome - let empty = email.value == "" || fullName.value != "" + let (requiredFieldsBody, setRequiredFieldsBody) = React.useState(_ => Dict.make()) + + let complete = areRequiredFieldsValid && !areRequiredFieldsEmpty && modalData->Option.isSome + let empty = areRequiredFieldsEmpty UtilityHooks.useHandlePostMessages(~complete, ~empty, ~paymentType="ach_bank_debit") @@ -75,18 +65,31 @@ let make = () => { } if complete { switch modalData { - | Some(data) => - let body = PaymentBody.achBankDebitBody( - ~email=email.value, - ~bank=data, - ~cardHolderName=fullName.value, - ~line1=line1.value, - ~line2=line2.value, - ~country=countryCode, - ~city=city.value, - ~postalCode=postalCode.value, - ~stateCode, - ) + | Some(data: ACHTypes.data) => + let body = + PaymentBody.dynamicPaymentBody("bank_debit", "ach") + ->getJsonFromArrayOfJson + ->flattenObject(true) + ->mergeTwoFlattenedJsonDicts(requiredFieldsBody) + ->getArrayOfTupleFromDict + ->Array.concat([ + ( + "payment_method_data.bank_debit.ach_bank_debit.account_number", + data.accountNumber->JSON.Encode.string, + ), + ( + "payment_method_data.bank_debit.ach_bank_debit.routing_number", + data.routingNumber->JSON.Encode.string, + ), + ( + "payment_method_data.bank_debit.ach_bank_debit.bank_account_holder_name", + data.accountHolderName->JSON.Encode.string, + ), + ( + "payment_method_data.bank_debit.ach_bank_debit.account_type", + data.accountType->JSON.Encode.string, + ), + ]) intent( ~bodyArr=body, ~confirmParam=confirm.confirmParams, @@ -100,7 +103,13 @@ let make = () => { postFailedSubmitResponse(~errortype="validation_error", ~message="Please enter all fields") } } - }, (email, modalData, fullName, isManualRetryEnabled)) + }, ( + modalData, + isManualRetryEnabled, + requiredFieldsBody, + areRequiredFieldsValid, + areRequiredFieldsEmpty, + )) useSubmitPaymentData(submitCallback) let paymentMethodType = "ach" @@ -112,8 +121,7 @@ let make = () => {
- - +
String.length > 0}> diff --git a/src/Payments/BacsBankDebit.res b/src/Payments/BacsBankDebit.res index 1d97cb76d..3ef3dedf9 100644 --- a/src/Payments/BacsBankDebit.res +++ b/src/Payments/BacsBankDebit.res @@ -1,5 +1,4 @@ open RecoilAtoms -open RecoilAtomTypes open Utils let formatSortCode = sortcode => { @@ -28,20 +27,12 @@ let make = () => { let {displaySavedPaymentMethods} = Recoil.useRecoilValueFromAtom(optionAtom) let intent = PaymentHelpers.usePaymentIntent(Some(loggerState), BankDebits) - let email = Recoil.useRecoilValueFromAtom(userEmailAddress) - let line1 = Recoil.useRecoilValueFromAtom(userAddressline1) - let line2 = Recoil.useRecoilValueFromAtom(userAddressline2) - let country = Recoil.useRecoilValueFromAtom(userAddressCountry) - let city = Recoil.useRecoilValueFromAtom(userAddressCity) - let postalCode = Recoil.useRecoilValueFromAtom(userAddressPincode) - let state = Recoil.useRecoilValueFromAtom(userAddressState) - let fullName = Recoil.useRecoilValueFromAtom(userFullName) let setComplete = Recoil.useSetRecoilState(fieldsComplete) let (sortcode, setSortcode) = React.useState(_ => "") let (accountNumber, setAccountNumber) = React.useState(_ => "") let paymentMethodListValue = Recoil.useRecoilValueFromAtom(PaymentUtils.paymentMethodListValue) - let countryCode = Utils.getCountryCode(country.value).isoAlpha2 - let stateCode = Utils.getStateCodeFromStateName(state.value, countryCode) + let areRequiredFieldsValid = Recoil.useRecoilValueFromAtom(areRequiredFieldsValid) + let areRequiredFieldsEmpty = Recoil.useRecoilValueFromAtom(areRequiredFieldsEmpty) let (sortCodeError, setSortCodeError) = React.useState(_ => "") @@ -57,25 +48,15 @@ let make = () => { let isVerifyPMAuthConnectorConfigured = displaySavedPaymentMethods && pmAuthMapper->Dict.get("sepa")->Option.isSome + let (requiredFieldsBody, setRequiredFieldsBody) = React.useState(_ => Dict.make()) + let complete = - email.value != "" && - email.isValid->Option.getOr(false) && + areRequiredFieldsValid && + !areRequiredFieldsEmpty && sortcode->cleanSortCode->String.length == 6 && - accountNumber != "" && - fullName.value != "" && - isAddressComplete(line1, state, city, country, postalCode) && - postalCode.isValid->Option.getOr(false) - - let empty = - email.value == "" || - sortcode == "" || - fullName.value != "" || - accountNumber == "" || - line1.value == "" && line2.value == "" || - city.value == "" || - postalCode.value == "" || - country.value == "" || - state.value == "" + accountNumber != "" + + let empty = areRequiredFieldsEmpty || sortcode == "" || accountNumber == "" UtilityHooks.useHandlePostMessages(~complete, ~empty, ~paymentType="bacs_bank_debit") @@ -90,18 +71,22 @@ let make = () => { if confirm.doSubmit { if complete { - let body = PaymentBody.bacsBankDebitBody( - ~email=email.value, - ~accNum=accountNumber, - ~sortCode=sortcode, - ~line1=line1.value, - ~line2=line2.value, - ~city=city.value, - ~zip=postalCode.value, - ~stateCode, - ~country=countryCode, - ~bankAccountHolderName=fullName.value, - ) + let body = + PaymentBody.dynamicPaymentBody("bank_debit", "bacs") + ->getJsonFromArrayOfJson + ->flattenObject(true) + ->mergeTwoFlattenedJsonDicts(requiredFieldsBody) + ->getArrayOfTupleFromDict + ->Array.concat([ + ( + "payment_method_data.bank_debit.bacs_bank_debit.account_number", + accountNumber->JSON.Encode.string, + ), + ( + "payment_method_data.bank_debit.bacs_bank_debit.sort_code", + sortcode->cleanSortCode->JSON.Encode.string, + ), + ]) intent( ~bodyArr=body, ~confirmParam=confirm.confirmParams, @@ -163,9 +148,7 @@ let make = () => { placeholder="00012345" />
- - - +
diff --git a/src/Payments/BankDebitModal.res b/src/Payments/BankDebitModal.res index 119f19034..7db38e49c 100644 --- a/src/Payments/BankDebitModal.res +++ b/src/Payments/BankDebitModal.res @@ -1,5 +1,6 @@ open CardUtils open ACHTypes +open DynamicFieldsUtils type focus = Routing | Account | NONE @@ -118,6 +119,8 @@ let make = (~setModalData) => { let (requiredFieldsBody, setRequiredFieldsBody) = React.useState(_ => Dict.make()) + let paymentMethodListValue = Recoil.useRecoilValueFromAtom(PaymentUtils.paymentMethodListValue) + let (openModal, setOpenModal) = React.useState(_ => false) let (accountNum, setAccountNum) = React.useState(_ => "") @@ -176,6 +179,39 @@ let make = (~setModalData) => { let isAchDebit = selectedOption->String.includes("ach_debit") let isBecsDebit = selectedOption->String.includes("becs_debit") + let paymentMethod = "bank_debit" + let paymentMethodType = switch (isAchDebit, isBecsDebit) { + | (true, _) => "ach" + | (_, true) => "becs" + | _ => "sepa" + } + + let paymentMethodTypes = PaymentUtils.usePaymentMethodTypeFromList( + ~paymentMethodListValue, + ~paymentMethod, + ~paymentMethodType, + ) + + let (superpositionMissingFields, initialValues, _) = useSuperpositionFields( + ~paymentMethod, + ~paymentMethodType, + ~paymentMethodTypes, + ~paymentMethodListValue, + ) + + let getRequiredFieldPath = (fieldType: PaymentMethodsRecord.paymentMethodsFields) => { + superpositionMissingFields + ->Array.find(r => { + let convertedFieldType = DynamicFieldsUtils.fieldTypeToPaymentMethodsField( + r.fieldType, + r.options, + ) + convertedFieldType === fieldType + }) + ->Option.map(r => r.outputPath) + ->Option.getOr(getBillingAddressPathFromFieldType(fieldType)) + } + let handleAccountHolderNameChange = ev => { let accName = ReactEvent.Form.target(ev)["value"] setAccountHolderName(_ => accName) @@ -204,67 +240,60 @@ let make = (~setModalData) => {
+ let formValidator = React.useMemo(() => { + _ => Dict.make() + }, []) + let nonDynamicFieldsModalBody = - <> -
- {React.string(localeString.billingDetailsText)} -
-
- -
-
- {React.string("Bank Details")} -
-
- {React.string("Account Holder Name")} -
- setInputFocus(_ => NONE)} - /> - -
- {React.string("IBAN")} -
- -
-
- -
+ ()} + initialValues={Some(initialValues)} + validate={Some(formValidator)} + render={_ => { + <> +
+ {React.string(localeString.billingDetailsText)} +
+
+ +
+
+ {React.string("Bank Details")} +
+
+ {React.string("Account Holder Name")} +
+ setInputFocus(_ => NONE)} + /> +
{ color: themeObj.colorText, marginBottom: "5px", }> - {React.string("Routing number")} + {React.string("IBAN")}
setInputFocus(_ => Routing)} + value=iban + onChange=changeIBAN + type_="text" + maxLength=42 + inputRef=ibanRef + placeholder="eg: DE00 0000 0000 0000 0000 00" /> +
+
+ +
+
+ {React.string("Routing number")} +
+ setInputFocus(_ => Routing)} + /> +
+
+ +
+
+ {React.string("Account number")} +
+ setInputFocus(_ => Account)} + onBlur={_ => setInputFocus(_ => NONE)} + /> +
+
- - -
+ +
+ +
+
+
{ color: themeObj.colorText, marginBottom: "5px", }> - {React.string("Account number")} + {React.string("BSB")}
setInputFocus(_ => Account)} - onBlur={_ => setInputFocus(_ => NONE)} + maxLength=7 + placeholder="eg: 000-000" /> -
-
-
- -
- +
-
- -
- {React.string("BSB")} -
- -
-