From a81387ab88e5dc01d9cf8ff1b7c6b9692f2f1d04 Mon Sep 17 00:00:00 2001 From: "aritro.ghosh" Date: Tue, 3 Dec 2024 16:02:41 +0530 Subject: [PATCH] feat: add address element for shipping and billing address --- src/AddressElement.res | 403 +++++++++++++++++++++ src/CardUtils.res | 2 + src/Components/EmailPaymentInput.res | 4 +- src/Components/FullNamePaymentInput.res | 9 +- src/Components/InputField.res | 4 +- src/Components/PhoneNumberPaymentInput.res | 6 +- src/Country.res | 10 + src/LoaderController.res | 6 + src/RenderPaymentMethods.res | 2 + src/Types/CardThemeType.res | 4 + src/Types/PaymentType.res | 33 ++ src/Utilities/RecoilAtoms.res | 1 + src/Utilities/Utils.res | 2 + src/Window.res | 2 + src/hyper-loader/Elements.res | 2 + src/hyper-loader/LoaderPaymentElement.res | 40 ++ src/hyper-loader/Types.res | 2 + 17 files changed, 525 insertions(+), 7 deletions(-) create mode 100644 src/AddressElement.res diff --git a/src/AddressElement.res b/src/AddressElement.res new file mode 100644 index 000000000..9446bd0c4 --- /dev/null +++ b/src/AddressElement.res @@ -0,0 +1,403 @@ +@react.component +let make = (~mode) => { + open RecoilAtoms + open Utils + + let {config, themeObj, localeString} = Recoil.useRecoilValueFromAtom(configAtom) + let {iframeId} = Recoil.useRecoilValueFromAtom(keys) + let isSpacedInnerLayout = config.appearance.innerLayout === Spaced + let spacedStylesForBiilingDetails = isSpacedInnerLayout ? "p-2" : "my-2" + let logger = Recoil.useRecoilValueFromAtom(loggerAtom) + let addressOptions = Recoil.useRecoilValueFromAtom(addressElementOptions) + let (fullName, setFullName) = Recoil.useLoggedRecoilState(userFullName, "fullName", logger) + let (email, setEmail) = Recoil.useLoggedRecoilState(userEmailAddress, "email", logger) + let (phone, setPhone) = Recoil.useLoggedRecoilState(userPhoneNumber, "phone", logger) + + let (line1, setLine1) = Recoil.useLoggedRecoilState(userAddressline1, "line1", logger) + let (line2, setLine2) = Recoil.useLoggedRecoilState(userAddressline2, "line2", logger) + let (city, setCity) = Recoil.useLoggedRecoilState(userAddressCity, "city", logger) + let (state, setState) = Recoil.useLoggedRecoilState(userAddressState, "state", logger) + let (postalCode, setPostalCode) = Recoil.useLoggedRecoilState( + userAddressPincode, + "postal_code", + logger, + ) + + let (country, setCountry) = Recoil.useLoggedRecoilState(userCountry, "country", logger) + let countryArr = Country.country->Array.map(item => item.countryName) + let updatedCountryArray = countryArr->DropdownField.updateArrayOfStringToOptionsTypeArray + let (stateJson, setStatesJson) = React.useState(_ => None) + let (complete, setComplete) = React.useState(_ => "false") + 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 isFieldOptional = field => Array.includes(addressOptions.optional, field) + + React.useEffect0(() => { + open Promise + AddressPaymentInput.importStates("./States.json") + ->then(res => { + setStatesJson(_ => Some(res.states)) + resolve() + }) + ->catch(_ => { + setStatesJson(_ => None) + resolve() + }) + ->ignore + + None + }) + + let checkFieldCompletion = () => { + let isNameComplete = isFieldOptional("full_name") ? true : fullName.value !== "" + let isEmailComplete = isFieldOptional("email") ? true : email.value !== "" + let isPhoneComplete = isFieldOptional("phone") ? true : phone.value !== "" + let isLine1Complete = isFieldOptional("line1") ? true : line1.value !== "" + let isLine2Complete = isFieldOptional("line2") ? true : line2.value !== "" + let isCityComplete = isFieldOptional("city") ? true : city.value !== "" + let isStateComplete = isFieldOptional("state") ? true : state.value !== "" + let isPostalComplete = isFieldOptional("postal_code") ? true : postalCode.value !== "" + let isCountryComplete = isFieldOptional("country") ? true : country !== "" + + setComplete(_ => + isNameComplete && + isEmailComplete && + isPhoneComplete && + isLine1Complete && + isLine2Complete && + isCityComplete && + isStateComplete && + isPostalComplete && + isCountryComplete + ? "true" + : "false" + ) + } + + let checkRequiredFields = () => { + if line1.value == "" && !isFieldOptional("line1") { + setLine1(prev => { + ...prev, + errorString: prev.errorString === "" + ? localeString.nameEmptyText("Address line 1") + : prev.errorString, + }) + } + if line2.value == "" && !isFieldOptional("line2") { + setLine2(prev => { + ...prev, + errorString: prev.errorString === "" + ? localeString.nameEmptyText("Address line 2") + : prev.errorString, + }) + } + if state.value == "" && !isFieldOptional("state") { + setState(prev => { + ...prev, + errorString: prev.errorString === "" + ? localeString.nameEmptyText("State name") + : prev.errorString, + }) + } + if postalCode.value == "" && !isFieldOptional("postal_code") { + setPostalCode(prev => { + ...prev, + errorString: prev.errorString === "" + ? localeString.nameEmptyText("postal code") + : prev.errorString, + }) + } + if city.value == "" && !isFieldOptional("city") { + setCity(prev => { + ...prev, + errorString: prev.errorString === "" + ? localeString.nameEmptyText("City name") + : prev.errorString, + }) + } + if email.value == "" && !isFieldOptional("email") { + setEmail(prev => { + ...prev, + errorString: prev.errorString === "" + ? localeString.nameEmptyText("Email") + : prev.errorString, + }) + } + if fullName.value == "" && !isFieldOptional("full_name") { + setFullName(prev => { + ...prev, + errorString: prev.errorString === "" + ? localeString.nameEmptyText("Full name") + : prev.errorString, + }) + } + if phone.value == "" && !isFieldOptional("phone") { + setPhone(prev => { + ...prev, + errorString: localeString.nameEmptyText("Phone number"), + }) + } + if country == "" && !isFieldOptional("country") { + setPhone(prev => { + ...prev, + errorString: localeString.nameEmptyText("Country"), + }) + } + } + + let getAddressDetails = () => { + let (firstName, lastName) = fullName.value->Utils.getFirstAndLastNameFromFullName + let addressDetails: PaymentType.addressData = { + complete: complete == "true" ? true : false, + data: { + first_name: firstName->getStringFromJson(""), + last_name: lastName->getStringFromJson(""), + line1: line1.value, + line2: line2.value, + city: city.value, + state: state.value, + postal_code: postalCode.value, + country, + email: email.value, + phone: phone.value, + country_code: phone.countryCode->Option.getOr(""), + }, + } + addressDetails + } + + React.useEffect(() => { + checkFieldCompletion() + None + }, [ + fullName.value, + email.value, + phone.value, + line1.value, + line2.value, + city.value, + state.value, + postalCode.value, + ]) + + React.useEffect0(() => { + Utils.messageParentWindow([("id", iframeId->JSON.Encode.string)]) + None + }) + + React.useEffect(() => { + let handleFun = (ev: Window.event) => { + let json = ev.data->safeParse + let dict = json->Utils.getDictFromJson + if dict->Dict.get("getBillingAddress")->Option.isSome && mode === "billing" { + let currentAddressDetails = getAddressDetails() + checkRequiredFields() + Utils.messageParentWindow([ + ("billingAddressDetails", currentAddressDetails->Identity.anyTypeToJson), + ]) + } else if dict->Dict.get("getShippingAddress")->Option.isSome && mode === "shipping" { + let currentAddressDetails = getAddressDetails() + checkRequiredFields() + Utils.messageParentWindow([ + ("shippingAddressDetails", currentAddressDetails->Identity.anyTypeToJson), + ]) + } + } + + handleMessage(handleFun, "Error in parsing sent Data") + }, [ + line1.value, + line2.value, + city.value, + state.value, + postalCode.value, + country, + email.value, + phone.value, + fullName.value, + complete, + ]) + + 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: "", + }) + } + } + + <> +
+
+ {React.string(mode ++ " details")} +
+
+ // Full Name + + // Address Line 1 + { + 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 !== ""), + }) + }} + paymentType={Payment} + type_="text" + name="line1" + inputRef=line1Ref + placeholder=localeString.line1Placeholder + className={isSpacedInnerLayout ? "" : "!border-b-0"} + /> + //Address Line 2 + { + 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 !== ""), + }) + }} + paymentType={Payment} + type_="text" + name="line2" + inputRef=line2Ref + placeholder={"Apt., unit number, etc"} + /> + // State and City +
+ { + 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 !== ""), + }) + }} + paymentType={Payment} + type_="text" + name="city" + inputRef=cityRef + placeholder=localeString.cityLabel + className={isSpacedInnerLayout ? "" : "!border-r-0"} + /> + {switch stateJson { + | Some(options) => + getStateNames({ + value: country, + isValid: None, + errorString: "", + })} + /> + | None => React.null + }} +
+ // Country and Pincode +
+ + { + let value = ReactEvent.Focus.target(ev)["value"] + setPostalCode(prev => { + ...prev, + isValid: Some(value !== ""), + }) + }} + onChange=onPostalChange + paymentType={Payment} + name="postal" + inputRef=postalRef + placeholder=localeString.postalCodeLabel + className={isSpacedInnerLayout ? "" : "!border-t-0"} + /> +
+ // Phone Number + + // Email + +
+
+ +} diff --git a/src/CardUtils.res b/src/CardUtils.res index 3bb719c6f..a6dcad8f7 100644 --- a/src/CardUtils.res +++ b/src/CardUtils.res @@ -367,6 +367,8 @@ let getCardBrandIcon = (cardType, paymentType) => { | ExpressCheckoutElement | PaymentMethodsManagement | PazeElement + | ShippingAddressElement + | BillingAddressElement | NONE => } diff --git a/src/Components/EmailPaymentInput.res b/src/Components/EmailPaymentInput.res index 3d8e13ded..b8fb05483 100644 --- a/src/Components/EmailPaymentInput.res +++ b/src/Components/EmailPaymentInput.res @@ -3,7 +3,7 @@ open PaymentType open Utils @react.component -let make = (~paymentType) => { +let make = (~paymentType, ~isOptional=false) => { let {localeString} = Recoil.useRecoilValueFromAtom(configAtom) let loggerState = Recoil.useRecoilValueFromAtom(loggerAtom) let (email, setEmail) = Recoil.useLoggedRecoilState(userEmailAddress, "email", loggerState) @@ -56,7 +56,7 @@ let make = (~paymentType) => { { +let make = ( + ~paymentType, + ~customFieldName=None, + ~optionalRequiredFields=None, + ~isOptional=false, +) => { let {localeString} = Recoil.useRecoilValueFromAtom(configAtom) let {fields} = Recoil.useRecoilValueFromAtom(optionAtom) let loggerState = Recoil.useRecoilValueFromAtom(loggerAtom) @@ -79,7 +84,7 @@ let make = (~paymentType, ~customFieldName=None, ~optionalRequiredFields=None) = + | PaymentMethodCollectElement + | ShippingAddressElement + | BillingAddressElement => setEleClassName(_ => val) | _ => () } diff --git a/src/Components/PhoneNumberPaymentInput.res b/src/Components/PhoneNumberPaymentInput.res index 31ba06cf9..416d8d11f 100644 --- a/src/Components/PhoneNumberPaymentInput.res +++ b/src/Components/PhoneNumberPaymentInput.res @@ -1,5 +1,5 @@ @react.component -let make = () => { +let make = (~isOptional=false) => { open RecoilAtoms open PaymentType open Utils @@ -59,6 +59,7 @@ let make = () => { ...prev, countryCode: valueDropDown->getCountryCodeSplitValue, value: val, + errorString: val === "" ? prev.errorString : "", }) } @@ -66,6 +67,7 @@ let make = () => { setPhone(prev => { ...prev, countryCode: valueDropDown->getCountryCodeSplitValue, + errorString: prev.value === "" ? prev.errorString : "", }) None }, [valueDropDown]) @@ -85,7 +87,7 @@ let make = () => { { | _ => country } } + +let getCountryNameFromIsoAlpha = (code: string): string => { + let upperCode = code->Js.String2.toUpperCase + switch country->Array.find(ele => { + ele.isoAlpha2 == upperCode || ele.isoAlpha3 == upperCode + }) { + | Some(val) => val.countryName + | None => "India" + } +} diff --git a/src/LoaderController.res b/src/LoaderController.res index b2a35c77f..5173da28c 100644 --- a/src/LoaderController.res +++ b/src/LoaderController.res @@ -42,6 +42,7 @@ let make = (~children, ~paymentMode, ~setIntegrateErrorError, ~logger, ~initTime let setIsPaymentButtonHandlerProvided = Recoil.useSetRecoilState( isPaymentButtonHandlerProvidedAtom, ) + let setAddressElementOptions = Recoil.useSetRecoilState(addressElementOptions) let optionsCallback = (optionsPayment: PaymentType.options) => { [ @@ -94,6 +95,11 @@ let make = (~children, ~paymentMode, ~setIntegrateErrorError, ~logger, ~initTime ) setPaymentMethodCollectOptions(_ => paymentMethodCollectOptions) } + | ShippingAddressElement + | BillingAddressElement => { + let addressOptions = PaymentType.itemToObjMapperAddress(optionsDict, logger) + setAddressElementOptions(_ => addressOptions) + } | GooglePayElement | PayPalElement | ApplePayElement diff --git a/src/RenderPaymentMethods.res b/src/RenderPaymentMethods.res index 4296496ce..9e60fd0d0 100644 --- a/src/RenderPaymentMethods.res +++ b/src/RenderPaymentMethods.res @@ -82,6 +82,8 @@ let make = ( paymentType cardProps expiryProps cvcProps zipProps handleElementFocus isFocus /> + | ShippingAddressElement => + | BillingAddressElement => | GooglePayElement | PayPalElement | ApplePayElement diff --git a/src/Types/CardThemeType.res b/src/Types/CardThemeType.res index 2584d8a68..b4f27ba1b 100644 --- a/src/Types/CardThemeType.res +++ b/src/Types/CardThemeType.res @@ -19,6 +19,8 @@ type mode = | PazeElement | ExpressCheckoutElement | PaymentMethodsManagement + | ShippingAddressElement + | BillingAddressElement | NONE type label = Above | Floating | Never type themeClass = { @@ -110,6 +112,8 @@ let getPaymentMode = val => { | "expressCheckout" => ExpressCheckoutElement | "paze" => PazeElement | "paymentMethodsManagement" => PaymentMethodsManagement + | "shippingAddressElement" => ShippingAddressElement + | "billingAddressElement" => BillingAddressElement | _ => NONE } } diff --git a/src/Types/PaymentType.res b/src/Types/PaymentType.res index 346efa081..0e24e4cbc 100644 --- a/src/Types/PaymentType.res +++ b/src/Types/PaymentType.res @@ -25,6 +25,26 @@ type address = { country: string, postal_code: string, } + +type addressDetails = { + first_name: string, + last_name: string, + line1: string, + line2: string, + city: string, + state: string, + country: string, + postal_code: string, + email: string, + phone: string, + country_code: string, +} + +type addressData = { + complete: bool, + data: addressDetails, +} + type addressType = | JSONString(string) | JSONObject(showAddress) @@ -185,6 +205,8 @@ type payerDetails = { phone: option, } +type addressOptions = {optional: array} + let defaultCardDetails = { scheme: None, last4Digits: "", @@ -327,6 +349,11 @@ let defaultOptions = { customMessageForCardTerms: "", } +let defaultOptional = [] +let defaultAddressOptions = { + optional: defaultOptional, +} + let getLayout = (str, logger) => { switch str { | "tabs" => Tabs @@ -1090,3 +1117,9 @@ let itemToPayerDetailsObjectMapper = dict => { ->Option.flatMap(Dict.get(_, "national_number")) ->Option.flatMap(JSON.Decode.string), } + +let itemToObjMapperAddress = (dict, logger) => { + { + optional: getStrArray(dict, "optional"), + } +} diff --git a/src/Utilities/RecoilAtoms.res b/src/Utilities/RecoilAtoms.res index b84380843..431876de7 100644 --- a/src/Utilities/RecoilAtoms.res +++ b/src/Utilities/RecoilAtoms.res @@ -4,6 +4,7 @@ let keys = Recoil.atom("keys", CommonHooks.defaultkeys) let configAtom = Recoil.atom("defaultRecoilConfig", CardTheme.defaultRecoilConfig) let portalNodes = Recoil.atom("portalNodes", PortalState.defaultDict) let elementOptions = Recoil.atom("elementOptions", ElementType.defaultOptions) +let addressElementOptions = Recoil.atom("addressElementOptions", PaymentType.defaultAddressOptions) let optionAtom = Recoil.atom("options", PaymentType.defaultOptions) let sessions = Recoil.atom("sessions", PaymentType.Loading) let paymentMethodList = Recoil.atom("paymentMethodList", PaymentType.Loading) diff --git a/src/Utilities/Utils.res b/src/Utilities/Utils.res index b52413668..35cce4efd 100644 --- a/src/Utilities/Utils.res +++ b/src/Utilities/Utils.res @@ -1298,6 +1298,8 @@ let expressCheckoutComponents = [ "paze", "samsungPay", "expressCheckout", + "shippingAddressElement", + "billingAddressElement", ] let spmComponents = ["paymentMethodCollect"]->Array.concat(expressCheckoutComponents) diff --git a/src/Window.res b/src/Window.res index 4c4263247..9c6ed25b6 100644 --- a/src/Window.res +++ b/src/Window.res @@ -40,6 +40,8 @@ external removeEventListener: (string, 'ev => unit) => unit = "removeEventListen @scope("window") @get external cardNumberElement: window => option = "cardNumber" @get external cardCVCElement: window => option = "cardCvc" @get external cardExpiryElement: window => option = "cardExpiry" +@get external shippingAddressElement: window => option = "shippingAddressElement" +@get external billingAddressElement: window => option = "billingAddressElement" @get external document: window => document = "document" @get external fullscreen: window => option = "fullscreen" @get external frames: window => {..} = "frames" diff --git a/src/hyper-loader/Elements.res b/src/hyper-loader/Elements.res index 49395e7f0..1a3cdef35 100644 --- a/src/hyper-loader/Elements.res +++ b/src/hyper-loader/Elements.res @@ -295,6 +295,8 @@ let make = ( | "paze" | "samsungPay" | "paymentMethodsManagement" + | "shippingAddressElement" + | "billingAddressElement" | "payment" => () | str => manageErrorWarning(UNKNOWN_KEY, ~dynamicStr=`${str} type in create`, ~logger) } diff --git a/src/hyper-loader/LoaderPaymentElement.res b/src/hyper-loader/LoaderPaymentElement.res index 533658fdc..2d20ab4e6 100644 --- a/src/hyper-loader/LoaderPaymentElement.res +++ b/src/hyper-loader/LoaderPaymentElement.res @@ -137,6 +137,45 @@ let make = ( }) } + let getAddressValues = () => { + Promise.make((resolve, _reject) => { + if componentType !== "billingAddressElement" && componentType !== "shippingAddressElement" { + resolve(JSON.Encode.null) + } + + iframeRef->Array.forEach(iframe => { + if componentType === "billingAddressElement" { + let message = [("getBillingAddress", true->JSON.Encode.bool)]->Dict.fromArray + iframe->Window.iframePostMessage(message) + } else { + let message = [("getShippingAddress", true->JSON.Encode.bool)]->Dict.fromArray + iframe->Window.iframePostMessage(message) + } + }) + + let handleFun = (ev: Types.event) => { + let json = ev.data->anyTypeToJson + let dict = json->Utils.getDictFromJson + + if ( + dict->Dict.get("billingAddressDetails")->Option.isSome && + componentType === "billingAddressElement" + ) { + let addressDetails = dict->Utils.getDictFromDict("billingAddressDetails") + resolve(addressDetails->Identity.anyTypeToJson) + } else if ( + dict->Dict.get("shippingAddressDetails")->Option.isSome && + componentType === "shippingAddressElement" + ) { + let addressDetails = dict->Utils.getDictFromDict("shippingAddressDetails") + resolve(addressDetails->Identity.anyTypeToJson) + } + } + + addSmartEventListener("message", handleFun, "showAddressHandler")->ignore + }) + } + let focus = () => { iframeRef->Array.forEach(iframe => { let message = [("doFocus", true->JSON.Encode.bool)]->Dict.fromArray @@ -420,6 +459,7 @@ let make = ( update, mount, onSDKHandleClick, + getAddressValues, } } catch { | e => { diff --git a/src/hyper-loader/Types.res b/src/hyper-loader/Types.res index 932612800..6ddba1603 100644 --- a/src/hyper-loader/Types.res +++ b/src/hyper-loader/Types.res @@ -32,6 +32,7 @@ type paymentElement = { focus: unit => unit, clear: unit => unit, onSDKHandleClick: option Promise.t> => unit, + getAddressValues: unit => Promise.t, } type element = { @@ -121,6 +122,7 @@ let defaultPaymentElement = { focus: () => (), clear: () => (), onSDKHandleClick: fnArgument => (), + getAddressValues: () => Promise.make((resolve, _) => resolve(Dict.make()->JSON.Encode.object)), } let create = (_componentType, _options) => {