From f4fc04d3e1d93b9c13260d61279f6af57619f393 Mon Sep 17 00:00:00 2001 From: Vrishab Srivatsa <136090360+vsrivatsa-juspay@users.noreply.github.com> Date: Tue, 5 Mar 2024 11:28:28 +0530 Subject: [PATCH] feat(paymentmethods): boleto Payment Method Integration (#195) Co-authored-by: ArushKapoorJuspay <121166031+ArushKapoorJuspay@users.noreply.github.com> --- public/icons/orca.svg | 3 + src/App.res | 12 ++- src/Components/InfoElement.res | 3 +- src/LocaleString.res | 11 ++- src/PaymentDetails.res | 5 ++ src/PaymentElement.res | 4 + src/Payments/Boleto.res | 115 ++++++++++++++++++++++++++ src/Payments/Boleto.resi | 5 ++ src/Payments/BoletoLazy.res | 8 ++ src/Payments/PaymentMethodsRecord.res | 10 ++- src/Payments/VoucherDisplay.res | 109 ++++++++++++++++++++++++ src/Types/PaymentConfirmTypes.res | 21 +++++ src/Types/PaymentModeType.res | 3 + src/Utilities/PaymentBody.res | 25 ++++++ src/Utilities/PaymentHelpers.res | 34 ++++++++ src/Utilities/PaymentUtils.res | 13 --- src/Window.res | 1 + src/orca-log-catcher/OrcaLogger.res | 2 + 18 files changed, 364 insertions(+), 20 deletions(-) create mode 100644 src/Payments/Boleto.res create mode 100644 src/Payments/Boleto.resi create mode 100644 src/Payments/BoletoLazy.res create mode 100644 src/Payments/VoucherDisplay.res diff --git a/public/icons/orca.svg b/public/icons/orca.svg index 6016bc4b1..7e48c5687 100644 --- a/public/icons/orca.svg +++ b/public/icons/orca.svg @@ -1209,4 +1209,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL + + + \ No newline at end of file diff --git a/src/App.res b/src/App.res index 9d486cdf3..998045ae1 100644 --- a/src/App.res +++ b/src/App.res @@ -12,16 +12,20 @@ let make = () => { log }) - React.useEffect1(()=>{ - setLoggerState(._=>logger) + React.useEffect1(() => { + setLoggerState(._ => logger) None - },[logger]) + }, [logger]) let renderFullscreen = { switch fullscreenMode { | "paymentloader" => - | "fullscreen" =>
+ | "fullscreen" => +
+ +
| "qrData" => + | "voucherData" => | "preMountLoader" => { let clientSecret = CardUtils.getQueryParamsDictforKey(url.search, "clientSecret") let sessionId = CardUtils.getQueryParamsDictforKey(url.search, "sessionId") diff --git a/src/Components/InfoElement.res b/src/Components/InfoElement.res index dab361cec..68968e84d 100644 --- a/src/Components/InfoElement.res +++ b/src/Components/InfoElement.res @@ -20,7 +20,8 @@ let make = () => { {switch selectedOption { | ACHTransfer | BacsTransfer - | SepaTransfer => + | SepaTransfer + | Boleto => localeString.bankDetailsText | _ => localeString.redirectText }->React.string} diff --git a/src/LocaleString.res b/src/LocaleString.res index c90354388..2fb5c49cb 100644 --- a/src/LocaleString.res +++ b/src/LocaleString.res @@ -61,6 +61,7 @@ type localeStrings = { nameEmptyText: string => string, completeNameEmptyText: string => string, billingDetailsText: string, + socialSecurityNumberLabel: string, } let defaultLocale = { @@ -137,6 +138,7 @@ let defaultLocale = { nameEmptyText: str => `Please provide your ${str}`, completeNameEmptyText: str => `Please provide your complete ${str}`, billingDetailsText: "Billing Details", + socialSecurityNumberLabel: "Social Security Number", } type locale = {localeStrings: array} @@ -215,6 +217,7 @@ let localeStrings = [ nameEmptyText: str => `Please provide your ${str}`, completeNameEmptyText: str => `Please provide your complete ${str}`, billingDetailsText: "Billing Details", + socialSecurityNumberLabel: "Social Security Number", }, { locale: "he", @@ -290,6 +293,7 @@ let localeStrings = [ nameEmptyText: str => `אנא ספק את שלך ${str}`, completeNameEmptyText: str => `אנא ספק את המלא שלך ${str}`, billingDetailsText: `פרטי תשלום`, + socialSecurityNumberLabel: `מספר ביטוח לאומי`, }, { locale: `fr`, @@ -364,7 +368,8 @@ let localeStrings = [ \"and": `et`, nameEmptyText: str => `Veuillez fournir votre ${str}`, completeNameEmptyText: str => `Veuillez fournir votre complet ${str}`, - billingDetailsText: "Détails de la facturation", + billingDetailsText: `Détails de la facturation`, + socialSecurityNumberLabel: `Numéro de sécurité sociale`, }, { locale: "en-GB", @@ -440,6 +445,7 @@ let localeStrings = [ nameEmptyText: str => `Please provide your ${str}`, completeNameEmptyText: str => `Please provide your complete ${str}`, billingDetailsText: "Billing Details", + socialSecurityNumberLabel: "Social Security Number", }, { locale: "ar", @@ -515,6 +521,7 @@ let localeStrings = [ nameEmptyText: str => `يرجى تقديم الخاص بك ${str}`, completeNameEmptyText: str => `يرجى تقديم كامل الخاص بك ${str}`, billingDetailsText: `تفاصيل الفاتورة`, + socialSecurityNumberLabel: `رقم الضمان الاجتماعي`, }, { locale: "ja", @@ -590,6 +597,7 @@ let localeStrings = [ nameEmptyText: str => `あなたの情報を提供してください ${str}`, completeNameEmptyText: str => `完全な情報を提供してください ${str}`, billingDetailsText: `支払明細`, + socialSecurityNumberLabel: `社会保障番号`, }, { locale: "de", @@ -665,5 +673,6 @@ let localeStrings = [ nameEmptyText: str => `Bitte geben Sie Ihre an ${str}`, completeNameEmptyText: str => `Bitte geben Sie Ihr vollständiges Formular an ${str}`, billingDetailsText: `Rechnungsdetails`, + socialSecurityNumberLabel: `Sozialversicherungsnummer`, }, ] diff --git a/src/PaymentDetails.res b/src/PaymentDetails.res index f76f7c140..a0534d357 100644 --- a/src/PaymentDetails.res +++ b/src/PaymentDetails.res @@ -97,4 +97,9 @@ let details = [ icon: Some(icon("bank", ~size=21)), displayName: "BECS Debit", }, + { + type_: "boleto", + icon: Some(icon("boleto", ~size=19)), + displayName: "Boleto", + }, ] diff --git a/src/PaymentElement.res b/src/PaymentElement.res index f7fb9960f..9c7b049bb 100644 --- a/src/PaymentElement.res +++ b/src/PaymentElement.res @@ -285,6 +285,10 @@ let make = ( | _ => React.null } + | Boleto => + + + | _ => diff --git a/src/Payments/Boleto.res b/src/Payments/Boleto.res new file mode 100644 index 000000000..c86b551f6 --- /dev/null +++ b/src/Payments/Boleto.res @@ -0,0 +1,115 @@ +open RecoilAtoms +open Utils + +let cleanSocialSecurityNumber = socialSecurityNumber => + socialSecurityNumber->Js.String2.replaceByRe(%re("/\D+/g"), "") + +let formatSocialSecurityNumber = socialSecurityNumber => { + let formatted = socialSecurityNumber->cleanSocialSecurityNumber + let firstPart = formatted->CardUtils.slice(0, 3) + let secondPart = formatted->CardUtils.slice(3, 6) + let thirdPart = formatted->CardUtils.slice(6, 9) + let fourthPart = formatted->CardUtils.slice(9, 11) + + if formatted->Js.String2.length <= 3 { + firstPart + } else if formatted->Js.String2.length > 3 && formatted->Js.String2.length <= 6 { + `${firstPart}.${secondPart}` + } else if formatted->Js.String2.length > 6 && formatted->Js.String2.length <= 9 { + `${firstPart}.${secondPart}.${thirdPart}` + } else { + `${firstPart}.${secondPart}.${thirdPart}-${fourthPart}` + } +} + +@react.component +let make = (~paymentType: CardThemeType.mode, ~list: PaymentMethodsRecord.list) => { + let loggerState = Recoil.useRecoilValueFromAtom(loggerAtom) + + let {config, themeObj, localeString} = Recoil.useRecoilValueFromAtom(configAtom) + let {iframeId} = Recoil.useRecoilValueFromAtom(keys) + + let intent = PaymentHelpers.usePaymentIntent(Some(loggerState), Other) + let setComplete = Recoil.useSetRecoilState(fieldsComplete) + let (socialSecurityNumber, setSocialSecurityNumber) = React.useState(_ => "") + + let (socialSecurityNumberError, setSocialSecurityNumberError) = React.useState(_ => "") + + let socialSecurityNumberRef = React.useRef(Js.Nullable.null) + + let (complete, empty) = React.useMemo1(() => { + ( + socialSecurityNumber->cleanSocialSecurityNumber->Js.String2.length == 11, + socialSecurityNumber->Js.String2.length == 0, + ) + }, [socialSecurityNumber]) + + React.useEffect2(() => { + handlePostMessageEvents(~complete, ~empty, ~paymentType="boleto", ~loggerState) + None + }, (complete, empty)) + + React.useEffect1(() => { + setComplete(._ => complete) + None + }, [complete]) + + let submitCallback = React.useCallback1((ev: Window.event) => { + let json = ev.data->Js.Json.parseExn + let confirm = json->Utils.getDictFromJson->ConfirmType.itemToObjMapper + + if confirm.doSubmit { + if complete { + let body = PaymentBody.boletoBody( + ~socialSecurityNumber=socialSecurityNumber->Js.String2.replaceByRe(%re("/\D+/g"), ""), + ) + intent( + ~bodyArr=body, + ~confirmParam=confirm.confirmParams, + ~handleUserError=false, + ~iframeId, + (), + ) + () + } else { + postFailedSubmitResponse(~errortype="validation_error", ~message="Please enter all fields") + } + } + }, [socialSecurityNumber]) + submitPaymentData(submitCallback) + + let changeSocialSecurityNumber = ev => { + let val = ReactEvent.Form.target(ev)["value"] + setSocialSecurityNumberError(_ => "") + setSocialSecurityNumber(_ => val->formatSocialSecurityNumber) + } + let socialSecurityNumberBlur = ev => { + let val = ReactEvent.Focus.target(ev)["value"]->cleanSocialSecurityNumber + if val->Js.String2.length != 11 && val->Js.String2.length > 0 { + setSocialSecurityNumberError(_ => "The social security number entered is invalid.") + } + } + +
+ + + +
+} + +let default = make diff --git a/src/Payments/Boleto.resi b/src/Payments/Boleto.resi new file mode 100644 index 000000000..f7493b29b --- /dev/null +++ b/src/Payments/Boleto.resi @@ -0,0 +1,5 @@ +let cleanSocialSecurityNumber: Js.String2.t => Js.String2.t +let formatSocialSecurityNumber: Js.String2.t => Js.String2.t +@react.component +let make: (~paymentType: CardThemeType.mode, ~list: PaymentMethodsRecord.list) => React.element +let default: props => React.element diff --git a/src/Payments/BoletoLazy.res b/src/Payments/BoletoLazy.res new file mode 100644 index 000000000..e6ef10a21 --- /dev/null +++ b/src/Payments/BoletoLazy.res @@ -0,0 +1,8 @@ +open LazyUtils + +type props = { + paymentType: CardThemeType.mode, + list: PaymentMethodsRecord.list, +} + +let make: props => React.element = reactLazy(.() => import_("./Boleto.bs.js")) diff --git a/src/Payments/PaymentMethodsRecord.res b/src/Payments/PaymentMethodsRecord.res index 6d6104f40..e2a26faaa 100644 --- a/src/Payments/PaymentMethodsRecord.res +++ b/src/Payments/PaymentMethodsRecord.res @@ -485,6 +485,13 @@ let paymentMethodsFields = [ displayName: "Pix", miniIcon: None, }, + { + paymentMethodName: "boleto", + icon: Some(icon("boleto", ~size=19, ~width=25)), + displayName: "Boleto", + fields: [InfoElement], + miniIcon: None, + }, ] type required_fields = { @@ -651,7 +658,7 @@ let getPaymentDetails = (arr: array) => { } type paymentMethod = - Cards | Wallets | PayLater | BankRedirect | BankTransfer | BankDebit | Crypto | NONE + Cards | Wallets | PayLater | BankRedirect | BankTransfer | BankDebit | Crypto | Voucher | NONE type cardType = Credit | Debit type paymentMethodType = @@ -737,6 +744,7 @@ let getMethod = str => { | "bank_transfer" => BankTransfer | "bank_debit" => BankDebit | "crypto" => Crypto + | "voucher" => Voucher | _ => NONE } } diff --git a/src/Payments/VoucherDisplay.res b/src/Payments/VoucherDisplay.res new file mode 100644 index 000000000..2a3d6b43e --- /dev/null +++ b/src/Payments/VoucherDisplay.res @@ -0,0 +1,109 @@ +open Utils +@react.component +let make = () => { + let (openModal, setOpenModal) = React.useState(_ => false) + let (returnUrl, setReturnUrl) = React.useState(_ => "") + let (downloadUrl, setDownloadUrl) = React.useState(_ => "") + let (reference, setReference) = React.useState(_ => "") + let logger = Recoil.useRecoilValueFromAtom(RecoilAtoms.loggerAtom) + let (downloadCounter, setDownloadCounter) = React.useState(_ => 0) + let (paymentMethod, setPaymentMethod) = React.useState(_ => "") + let (paymentIntent, setPaymentIntent) = React.useState(_ => Js.Json.null) + let (loader, setLoader) = React.useState(_ => true) + let linkRef = React.useRef(Js.Nullable.null) + + React.useEffect1(() => { + switch linkRef.current->Js.Nullable.toOption { + | Some(link) => link->Window.click + | None => () + } + None + }, [loader]) + + React.useEffect0(() => { + handlePostMessage([("iframeMountedCallback", true->Js.Json.boolean)]) + let handle = (ev: Window.event) => { + let json = ev.data->Js.Json.parseExn + let dict = json->Utils.getDictFromJson + if dict->Js.Dict.get("fullScreenIframeMounted")->Belt.Option.isSome { + let metadata = dict->getJsonObjectFromDict("metadata") + let metaDataDict = + metadata->Js.Json.decodeObject->Belt.Option.getWithDefault(Js.Dict.empty()) + setReturnUrl(_ => metaDataDict->getString("returnUrl", "")) + setDownloadUrl(_ => metaDataDict->getString("voucherUrl", "")) + setReference(_ => metaDataDict->getString("reference", "")) + setPaymentMethod(_ => metaDataDict->getString("paymentMethod", "")) + setPaymentIntent(_ => metaDataDict->getJsonObjectFromDict("payment_intent_data")) + setLoader(_ => false) + } + } + Window.addEventListener("message", handle) + Some(() => {Window.removeEventListener("message", handle)}) + }) + + let closeModal = () => { + postSubmitResponse(~jsonData=paymentIntent, ~url=returnUrl) + Modal.close(setOpenModal) + } + + +
+
+
+

+ {React.string( + `${paymentMethod->snakeToTitleCase} voucher was successfully generated! If the document hasn't started downloading automatically, click `, + )} + ReactDOM.Ref.domRef} + onClick={_ => { + setDownloadCounter(c => c + 1) + LoggerUtils.handleLogging( + ~optLogger=Some(logger), + ~value=downloadCounter->Js.Int.toString, + ~eventName=DISPLAY_VOUCHER, + ~paymentMethod, + (), + ) + }}> + {React.string("here")} + + {React.string(" to download it.")} +

+
+
+

+ {React.string("Bar Code Reference: ")} + + {React.string(reference)} + +

+
+
+ {React.string( + "Please do not close until you have successfully downloaded the voucher, after which you will be automatically redirected.", + )} +
+
+
+ +
+
+
+
+
+} diff --git a/src/Types/PaymentConfirmTypes.res b/src/Types/PaymentConfirmTypes.res index 8be8a2015..e1c7831ef 100644 --- a/src/Types/PaymentConfirmTypes.res +++ b/src/Types/PaymentConfirmTypes.res @@ -28,12 +28,18 @@ type redirectToUrl = { url: string, } +type voucherDetails = { + download_url: string, + reference: string, +} + type nextAction = { redirectToUrl: string, type_: string, bank_transfer_steps_and_charges_details: option, session_token: option, image_data_url: option, + voucher_details: option, display_to_timestamp: option, } type intent = { @@ -57,6 +63,7 @@ let defaultNextAction = { bank_transfer_steps_and_charges_details: None, session_token: None, image_data_url: None, + voucher_details: None, display_to_timestamp: None, } let defaultIntent = { @@ -106,6 +113,14 @@ let getBankTransferDetails = (dict, str) => { } }) } + +let getVoucherDetails = json => { + { + download_url: getString(json, "download_url", ""), + reference: getString(json, "reference", ""), + } +} + let getNextAction = (dict, str) => { dict ->Js.Dict.get(str) @@ -131,6 +146,12 @@ let getNextAction = (dict, str) => { ->Belt.Option.flatMap(Js.Json.decodeNumber) ->Belt.Option.getWithDefault(0.0), ), + voucher_details: { + json + ->Js.Dict.get("voucher_details") + ->Belt.Option.flatMap(Js.Json.decodeObject) + ->Belt.Option.map(json => json->getVoucherDetails) + }, } }) ->Belt.Option.getWithDefault(defaultNextAction) diff --git a/src/Types/PaymentModeType.res b/src/Types/PaymentModeType.res index 48d0d17d7..75503aaab 100644 --- a/src/Types/PaymentModeType.res +++ b/src/Types/PaymentModeType.res @@ -18,6 +18,7 @@ type payment = | BanContactCard | GooglePay | ApplePay + | Boleto | NONE let paymentMode = str => { @@ -41,6 +42,7 @@ let paymentMode = str => { | "bancontact_card" => BanContactCard | "google_pay" => GooglePay | "apple_pay" => ApplePay + | "boleto" => Boleto | _ => NONE } } @@ -66,4 +68,5 @@ let defaultOrder = [ "paypal", "crypto", "bancontact_card", + "boleto", ] diff --git a/src/Utilities/PaymentBody.res b/src/Utilities/PaymentBody.res index 1c2cca1ff..b7a7b70e0 100644 --- a/src/Utilities/PaymentBody.res +++ b/src/Utilities/PaymentBody.res @@ -30,6 +30,31 @@ let bancontactBody = () => [ ("payment_method_type", "bancontact_card"->Js.Json.string), ] +let boletoBody = (~socialSecurityNumber) => [ + ("payment_method", "voucher"->Js.Json.string), + ("payment_method_type", "boleto"->Js.Json.string), + ( + "payment_method_data", + [ + ( + "voucher", + [ + ( + "boleto", + [("social_security_number", socialSecurityNumber->Js.Json.string)] + ->Js.Dict.fromArray + ->Js.Json.object_, + ), + ] + ->Js.Dict.fromArray + ->Js.Json.object_, + ), + ] + ->Js.Dict.fromArray + ->Js.Json.object_, + ), +] + let savedCardBody = (~paymentToken, ~customerId, ~cvcNumber) => [ ("payment_method", "card"->Js.Json.string), ("payment_token", paymentToken->Js.Json.string), diff --git a/src/Utilities/PaymentHelpers.res b/src/Utilities/PaymentHelpers.res index 229794393..30bd4b439 100644 --- a/src/Utilities/PaymentHelpers.res +++ b/src/Utilities/PaymentHelpers.res @@ -284,6 +284,40 @@ let intentCall = ( ("iframeId", iframeId->Js.Json.string), ("metadata", metaData->Js.Json.object_), ]) + } else if intent.nextAction.type_ === "display_voucher_information" { + let voucherData = intent.nextAction.voucher_details->Belt.Option.getWithDefault({ + download_url: "", + reference: "", + }) + let headerObj = Js.Dict.empty() + headers->Js.Array2.forEach( + entries => { + let (x, val) = entries + Js.Dict.set(headerObj, x, val->Js.Json.string) + }, + ) + let metaData = + [ + ("voucherUrl", voucherData.download_url->Js.Json.string), + ("reference", voucherData.reference->Js.Json.string), + ("returnUrl", url.href->Js.Json.string), + ("paymentMethod", paymentMethod->Js.Json.string), + ("payment_intent_data", data), + ]->Js.Dict.fromArray + handleLogging( + ~optLogger, + ~value="", + ~internalMetadata=metaData->Js.Json.object_->Js.Json.stringify, + ~eventName=DISPLAY_VOUCHER, + ~paymentMethod, + (), + ) + handlePostMessage([ + ("fullscreen", true->Js.Json.boolean), + ("param", `voucherData`->Js.Json.string), + ("iframeId", iframeId->Js.Json.string), + ("metadata", metaData->Js.Json.object_), + ]) } else if intent.nextAction.type_ == "third_party_sdk_session_token" { let session_token = switch intent.nextAction.session_token { | Some(token) => token->Utils.getDictFromJson diff --git a/src/Utilities/PaymentUtils.res b/src/Utilities/PaymentUtils.res index f4ef8f1e1..fb0564cdd 100644 --- a/src/Utilities/PaymentUtils.res +++ b/src/Utilities/PaymentUtils.res @@ -182,19 +182,6 @@ let getConnectors = (list: PaymentMethodsRecord.list, method: connectorType) => | None => ([], []) } } -let getPaymentDetails = (arr: array) => { - let finalArr = [] - arr - ->Js.Array2.map(item => { - let optionalVal = PaymentDetails.details->Js.Array2.find(i => i.type_ == item) - switch optionalVal { - | Some(val) => finalArr->Js.Array2.push(val)->ignore - | None => () - } - }) - ->ignore - finalArr -} let getDisplayNameAndIcon = ( customNames: PaymentType.customMethodNames, paymentMethodName, diff --git a/src/Window.res b/src/Window.res index 808c7a496..4b327b81c 100644 --- a/src/Window.res +++ b/src/Window.res @@ -58,6 +58,7 @@ external style: Dom.element => style = "style" @set external setTransition: (style, string) => unit = "transition" @set external setHeight: (style, string) => unit = "height" @send external paymentRequest: (Js.Json.t, Js.Json.t, Js.Json.t) => Js.Json.t = "PaymentRequest" +@send external click: Dom.element => unit = "click" let iframePostMessage = (iframeRef: Js.nullable, message) => { switch iframeRef->Js.Nullable.toOption { diff --git a/src/orca-log-catcher/OrcaLogger.res b/src/orca-log-catcher/OrcaLogger.res index 9da71dd2a..c0c9671df 100644 --- a/src/orca-log-catcher/OrcaLogger.res +++ b/src/orca-log-catcher/OrcaLogger.res @@ -53,6 +53,7 @@ type eventName = | REDIRECTING_USER | DISPLAY_BANK_TRANSFER_INFO_PAGE | DISPLAY_QR_CODE_INFO_PAGE + | DISPLAY_VOUCHER | PAYMENT_METHODS_RESPONSE | LOADER_CHANGED @@ -109,6 +110,7 @@ let eventNameToStrMapper = eventName => { | REDIRECTING_USER => "REDIRECTING_USER" | DISPLAY_BANK_TRANSFER_INFO_PAGE => "DISPLAY_BANK_TRANSFER_INFO_PAGE" | DISPLAY_QR_CODE_INFO_PAGE => "DISPLAY_QR_CODE_INFO_PAGE" + | DISPLAY_VOUCHER => "DISPLAY_VOUCHER" | PAYMENT_METHODS_RESPONSE => "PAYMENT_METHODS_RESPONSE" | LOADER_CHANGED => "LOADER_CHANGED" }