From a4ffecf0cc31a3d094eb0f8c57d9d8260d06204b Mon Sep 17 00:00:00 2001 From: Pritish Budhiraja <1805317@kiit.ac.in> Date: Tue, 12 Mar 2024 02:40:34 +0530 Subject: [PATCH] feat: Support to handle confirm button (E2E) (#198) Co-authored-by: Vrishab Srivatsa <136090360+vsrivatsa-juspay@users.noreply.github.com> Co-authored-by: Vrishab Srivatsa Co-authored-by: Arush --- Hyperswitch-React-Demo-App/server.js | 1 - Hyperswitch-React-Demo-App/src/Cart.js | 8 +-- src/BrutalTheme.res | 8 +++ src/CardTheme.res | 41 +++++++++++++++ src/CharcoalTheme.res | 8 +++ src/Components/PayNowButton.res | 67 +++++++++++++++++------- src/DefaultTheme.res | 8 +++ src/Hooks/CommonHooks.res | 7 --- src/LoaderController.res | 4 +- src/MidnightTheme.res | 8 +++ src/NoTheme.res | 8 +++ src/PaymentElement.res | 17 +++--- src/SoftTheme.res | 8 +++ src/Types/CardThemeType.res | 8 +++ src/Types/PaymentType.res | 30 +++++++++++ src/Utilities/PaymentBody.res | 13 +++++ src/Utilities/Utils.res | 24 +++++++++ src/orca-loader/Elements.res | 2 - src/orca-loader/Hyper.res | 11 ++++ src/orca-loader/LoaderPaymentElement.res | 10 +--- src/orca-loader/OrcaUtils.res | 4 -- 21 files changed, 243 insertions(+), 52 deletions(-) diff --git a/Hyperswitch-React-Demo-App/server.js b/Hyperswitch-React-Demo-App/server.js index 991a4b95..caddd6e0 100644 --- a/Hyperswitch-React-Demo-App/server.js +++ b/Hyperswitch-React-Demo-App/server.js @@ -54,7 +54,6 @@ app.get("/create-payment-intent", async (req, res) => { amount: 2999, }, ], - currency: "USD", confirm: false, capture_method: "automatic", authentication_type: "three_ds", diff --git a/Hyperswitch-React-Demo-App/src/Cart.js b/Hyperswitch-React-Demo-App/src/Cart.js index 615d7ea2..24c19cf5 100644 --- a/Hyperswitch-React-Demo-App/src/Cart.js +++ b/Hyperswitch-React-Demo-App/src/Cart.js @@ -9,8 +9,8 @@ function Cart() {
{" "} - Hyperswicth - Playground App + {" "} + Hyperswitch Playground App
Test Mode
@@ -19,7 +19,7 @@ function Cart() {
- +
HS Tshirt
@@ -37,7 +37,7 @@ function Cart() {
- +
HS Cap
diff --git a/src/BrutalTheme.res b/src/BrutalTheme.res index bf897d61..72396067 100644 --- a/src/BrutalTheme.res +++ b/src/BrutalTheme.res @@ -38,6 +38,14 @@ let brutal = { colorIconCardError: "#ff1a1a", spacingGridColumn: "20px", spacingGridRow: "20px", + buttonBackgroundColor: "#f5fb1f", + buttonHeight: "48px", + buttonWidth: "thin", + buttonBorderRadius: "6px", + buttonBorderColor: "#566186", + buttonTextColor: "#000000", + buttonTextFontSize: "16px", + buttonTextFontWeight: "500", } let brutalRules = (theme: CardThemeType.themeClass) => { diff --git a/src/CardTheme.res b/src/CardTheme.res index 28c3d6da..bc0e74bf 100644 --- a/src/CardTheme.res +++ b/src/CardTheme.res @@ -128,6 +128,14 @@ let getVariables = (str, dict, default, logger) => { "spacingGridColumn", "spacingGridRow", "spacingAccordionItem", + "buttonBackgroundColor", + "buttonHeight", + "buttonWidth", + "buttonBorderRadius", + "buttonBorderColor", + "buttonTextColor", + "buttonTextFontSize", + "buttonTextFontWeight", ] unknownKeysWarning(validKeys, json, "appearance.variables", ~logger) { @@ -244,6 +252,39 @@ let getVariables = (str, dict, default, logger) => { ~logger, ), spacingGridRow: getWarningString(json, "spacingGridRow", default.spacingGridRow, ~logger), + buttonBackgroundColor: getWarningString( + json, + "buttonBackgroundColor", + default.spacingGridRow, + ~logger, + ), + buttonHeight: getWarningString(json, "buttonHeight", default.spacingGridRow, ~logger), + buttonWidth: getWarningString(json, "buttonWidth", default.spacingGridRow, ~logger), + buttonBorderRadius: getWarningString( + json, + "buttonBorderRadius", + default.spacingGridRow, + ~logger, + ), + buttonBorderColor: getWarningString( + json, + "buttonBorderColor", + default.spacingGridRow, + ~logger, + ), + buttonTextColor: getWarningString(json, "buttonTextColor", default.spacingGridRow, ~logger), + buttonTextFontSize: getWarningString( + json, + "buttonTextFontSize", + default.spacingGridRow, + ~logger, + ), + buttonTextFontWeight: getWarningString( + json, + "buttonTextFontWeight", + default.spacingGridRow, + ~logger, + ), } }) ->Belt.Option.getWithDefault(default) diff --git a/src/CharcoalTheme.res b/src/CharcoalTheme.res index 32a755ed..d42347b9 100644 --- a/src/CharcoalTheme.res +++ b/src/CharcoalTheme.res @@ -38,6 +38,14 @@ let charcoal = { colorIconCardError: "#fd1717", spacingGridColumn: "20px", spacingGridRow: "20px", + buttonBackgroundColor: "#000000", + buttonHeight: "48px", + buttonWidth: "thin", + buttonBorderRadius: "6px", + buttonBorderColor: "#000000", + buttonTextColor: "#ffffff", + buttonTextFontSize: "16px", + buttonTextFontWeight: "500", } let charcoalRules = theme => diff --git a/src/Components/PayNowButton.res b/src/Components/PayNowButton.res index aa5b8dda..def4bbdf 100644 --- a/src/Components/PayNowButton.res +++ b/src/Components/PayNowButton.res @@ -1,6 +1,6 @@ -@send external postMessage: (Window.window, Js.Json.t, string) => unit = "postMessage" +@send external postMessage: (Window.window, JSON.t, string) => unit = "postMessage" -external eventToJson: ReactEvent.Mouse.t => Js.Json.t = "%identity" +external eventToJson: ReactEvent.Mouse.t => JSON.t = "%identity" module Loader = { @react.component @@ -14,41 +14,72 @@ module Loader = { } } @react.component -let make = () => { - let {themeObj, localeString} = Recoil.useRecoilValueFromAtom(RecoilAtoms.configAtom) +let make = ( + ~cvcProps: CardUtils.cvcProps, + ~cardProps: CardUtils.cardProps, + ~expiryProps: CardUtils.expiryProps, + ~selectedOption: PaymentModeType.payment, +) => { + open RecoilAtoms + let {themeObj, localeString} = Recoil.useRecoilValueFromAtom(configAtom) let (isDisabled, setIsDisabled) = React.useState(() => false) let (showLoader, setShowLoader) = React.useState(() => false) - let complete = Recoil.useRecoilValueFromAtom(RecoilAtoms.fieldsComplete) + let areRequiredFieldsValidValue = Recoil.useRecoilValueFromAtom(areRequiredFieldsValid) + let {sdkHandleConfirmPayment} = Recoil.useRecoilValueFromAtom(optionAtom) - let handleOnClick = _ev => { + let (isCVCValid, _, _, _, _, _, _, _, _, _) = cvcProps + let (isCardValid, _, _, _, _, _, _, _, _, _) = cardProps + let (isExpiryValid, _, _, _, _, _, _, _, _) = expiryProps + + let validFormat = + isCVCValid->Option.getOr(false) && + isCardValid->Option.getOr(false) && + isExpiryValid->Option.getOr(false) && + areRequiredFieldsValidValue + + let confirmPayload = sdkHandleConfirmPayment->PaymentBody.confirmPayloadForSDKButton + + let handleOnClick = _ => { setIsDisabled(_ => true) setShowLoader(_ => true) - Utils.handleOnConfirmPostMessage(~targetOrigin="*", ()) + Utils.handlePostMessage([("handleSdkConfirm", confirmPayload)]) } - React.useEffect1(() => { - setIsDisabled(_ => !complete) + React.useEffect3(() => { + if selectedOption === Card { + setIsDisabled(_ => !validFormat) + } else { + setIsDisabled(_ => !areRequiredFieldsValidValue) + } None - }, [complete]) + }, (validFormat, areRequiredFieldsValidValue, selectedOption)) -
+
diff --git a/src/DefaultTheme.res b/src/DefaultTheme.res index 16f84b73..9077766f 100644 --- a/src/DefaultTheme.res +++ b/src/DefaultTheme.res @@ -38,6 +38,14 @@ let default = { colorIconCardError: "#fd1717", spacingGridColumn: "20px", spacingGridRow: "20px", + buttonBackgroundColor: "#006df9", + buttonHeight: "48px", + buttonWidth: "thin", + buttonBorderRadius: "6px", + buttonBorderColor: "#ffffff", + buttonTextColor: "#ffffff", + buttonTextFontSize: "16px", + buttonTextFontWeight: "500", } let defaultRules = theme => { diff --git a/src/Hooks/CommonHooks.res b/src/Hooks/CommonHooks.res index b5fd1f56..228df4b5 100644 --- a/src/Hooks/CommonHooks.res +++ b/src/Hooks/CommonHooks.res @@ -13,7 +13,6 @@ type keys = { publishableKey: string, iframeId: string, parentURL: string, - sdkHandleConfirmPayment: bool, sdkHandleOneClickConfirmPayment: bool, } @val @scope("document") external querySelector: string => Js.Nullable.t = "querySelector" @@ -86,11 +85,6 @@ let updateKeys = (dict, keyPair, setKeys) => { ...prev, parentURL: dict->Utils.getString(key, valueStr), }) - | "sdkHandleConfirmPayment" => - setKeys(.prev => { - ...prev, - sdkHandleConfirmPayment: dict->Utils.getBool(key, valueBool(false)), - }) | "sdkHandleOneClickConfirmPayment" => setKeys(.prev => { ...prev, @@ -105,6 +99,5 @@ let defaultkeys = { publishableKey: "", iframeId: "", parentURL: "*", - sdkHandleConfirmPayment: false, sdkHandleOneClickConfirmPayment: true, } diff --git a/src/LoaderController.res b/src/LoaderController.res index d9e861c8..0ca9ac8e 100644 --- a/src/LoaderController.res +++ b/src/LoaderController.res @@ -137,7 +137,8 @@ let make = (~children, ~paymentMode, ~setIntegrateErrorError, ~logger) => { setList(._ => updatedState) logger.setLogInfo(~value="SemiLoaded", ~eventName=LOADER_CHANGED, ()) } - | LoadError(x) => logger.setLogError( + | LoadError(x) => + logger.setLogError( ~value="LoadError: " ++ x->Js.Json.stringify, ~eventName=LOADER_CHANGED, (), @@ -269,7 +270,6 @@ let make = (~children, ~paymentMode, ~setIntegrateErrorError, ~logger) => { ("iframeId", "no-element"->Js.Json.string), ("publishableKey", ""->Js.Json.string), ("parentURL", "*"->Js.Json.string), - ("sdkHandleConfirmPayment", false->Js.Json.boolean), ("sdkHandleOneClickConfirmPayment", true->Js.Json.boolean), ]->Js.Array2.forEach(keyPair => { dict->CommonHooks.updateKeys(keyPair, setKeys) diff --git a/src/MidnightTheme.res b/src/MidnightTheme.res index 29d626d2..b4633793 100644 --- a/src/MidnightTheme.res +++ b/src/MidnightTheme.res @@ -38,6 +38,14 @@ let midnight = { colorIconCardError: "#fd1717", spacingGridColumn: "20px", spacingGridRow: "20px", + buttonBackgroundColor: "#85d996", + buttonHeight: "48px", + buttonWidth: "thin", + buttonBorderRadius: "6px", + buttonBorderColor: "#85d996", + buttonTextColor: "#000000", + buttonTextFontSize: "16px", + buttonTextFontWeight: "500", } let midnightRules = theme => diff --git a/src/NoTheme.res b/src/NoTheme.res index 7cfb7444..9f9df6f5 100644 --- a/src/NoTheme.res +++ b/src/NoTheme.res @@ -39,6 +39,14 @@ let nakedValues = { colorIconCardError: "#fd1717", spacingGridColumn: "20px", spacingGridRow: "20px", + buttonBackgroundColor: "", + buttonHeight: "48px", + buttonWidth: "thin", + buttonBorderRadius: "6px", + buttonBorderColor: "", + buttonTextColor: "", + buttonTextFontSize: "16px", + buttonTextFontWeight: "500", } let nakedValuesRules = _ => Js.Dict.empty()->Js.Json.object_ diff --git a/src/PaymentElement.res b/src/PaymentElement.res index f738c2ba..e373568b 100644 --- a/src/PaymentElement.res +++ b/src/PaymentElement.res @@ -30,7 +30,7 @@ let make = ( let (sessions, setSessions) = React.useState(_ => Js.Dict.empty()->Js.Json.object_) let (paymentOptions, setPaymentOptions) = React.useState(_ => []) let (walletOptions, setWalletOptions) = React.useState(_ => []) - let {sdkHandleConfirmPayment} = Recoil.useRecoilValueFromAtom(keys) + let {sdkHandleConfirmPayment} = Recoil.useRecoilValueFromAtom(optionAtom) let (list, setList) = React.useState(_ => PaymentMethodsRecord.defaultList) let (cardsContainerWidth, setCardsContainerWidth) = React.useState(_ => 0) @@ -392,11 +392,16 @@ let make = ( | Accordion => }}
- -
- -
-
+ + +
+ PaymentModeType.paymentMode} + /> +
{switch methodslist { diff --git a/src/SoftTheme.res b/src/SoftTheme.res index 9038d8cd..95d4b028 100644 --- a/src/SoftTheme.res +++ b/src/SoftTheme.res @@ -39,6 +39,14 @@ let soft = { colorIconCardError: "#fe87a1", spacingGridColumn: "20px", spacingGridRow: "20px", + buttonBackgroundColor: "#3c3d3e", + buttonHeight: "48px", + buttonWidth: "thin", + buttonBorderRadius: "6px", + buttonBorderColor: "#7d8fff", + buttonTextColor: "#7d8fff", + buttonTextFontSize: "16px", + buttonTextFontWeight: "500", } let softRules = theme => diff --git a/src/Types/CardThemeType.res b/src/Types/CardThemeType.res index 97505436..ccaa1c05 100644 --- a/src/Types/CardThemeType.res +++ b/src/Types/CardThemeType.res @@ -51,6 +51,14 @@ type themeClass = { colorIconCardError: string, spacingGridColumn: string, spacingGridRow: string, + buttonBackgroundColor: string, + buttonHeight: string, + buttonWidth: string, + buttonBorderRadius: string, + buttonBorderColor: string, + buttonTextColor: string, + buttonTextFontSize: string, + buttonTextFontWeight: string, } type appearance = { theme: theme, diff --git a/src/Types/PaymentType.res b/src/Types/PaymentType.res index a5c2784b..586c310a 100644 --- a/src/Types/PaymentType.res +++ b/src/Types/PaymentType.res @@ -132,6 +132,11 @@ type billingAddress = { usePrefilledValues: showType, } +type sdkHandleConfirmPayment = { + handleConfirm: bool, + confirmParams: ConfirmType.confirmParams, +} + type options = { defaultValues: defaultValues, layout: layoutType, @@ -149,6 +154,7 @@ type options = { payButtonStyle: style, showCardFormByDefault: bool, billingAddress: billingAddress, + sdkHandleConfirmPayment: sdkHandleConfirmPayment, } let defaultCardDetails = { scheme: None, @@ -250,6 +256,12 @@ let defaultBillingAddress = { isUseBillingAddress: false, usePrefilledValues: Auto, } + +let defaultSdkHandleConfirmPayment = { + handleConfirm: false, + confirmParams: ConfirmType.defaultConfirm, +} + let defaultOptions = { defaultValues: defaultDefaultValues, business: defaultBusiness, @@ -267,6 +279,7 @@ let defaultOptions = { customMethodNames: [], showCardFormByDefault: true, billingAddress: defaultBillingAddress, + sdkHandleConfirmPayment: defaultSdkHandleConfirmPayment, } let getLayout = (str, logger) => { switch str { @@ -873,6 +886,19 @@ let getBillingAddress = (dict, str, logger) => { ->Belt.Option.getWithDefault(defaultBillingAddress) } +let getConfirmParams = dict => { + open ConfirmType + { + return_url: dict->getString("return_url", ""), + publishableKey: dict->getString("publishableKey", ""), + } +} + +let getSdkHandleConfirmPaymentProps = dict => { + handleConfirm: dict->getBool("handleConfirm", false), + confirmParams: dict->getDictfromDict("confirmParams")->getConfirmParams, +} + let itemToObjMapper = (dict, logger) => { unknownKeysWarning( [ @@ -890,6 +916,7 @@ let itemToObjMapper = (dict, logger) => { "displaySavedPaymentMethods", "sdkHandleOneClickConfirmPayment", "showCardFormByDefault", + "sdkHandleConfirmPayment", ], dict, "options", @@ -925,6 +952,9 @@ let itemToObjMapper = (dict, logger) => { payButtonStyle: getStyle(dict, "payButtonStyle", logger), showCardFormByDefault: getBool(dict, "showCardFormByDefault", true), billingAddress: getBillingAddress(dict, "billingAddress", logger), + sdkHandleConfirmPayment: dict + ->getDictfromDict("sdkHandleConfirmPayment") + ->getSdkHandleConfirmPaymentProps, } } diff --git a/src/Utilities/PaymentBody.res b/src/Utilities/PaymentBody.res index dacfac09..ffc308d6 100644 --- a/src/Utilities/PaymentBody.res +++ b/src/Utilities/PaymentBody.res @@ -95,6 +95,19 @@ let mandateBody = paymentType => { ] } +let confirmPayloadForSDKButton = (sdkHandleConfirmPayment: PaymentType.sdkHandleConfirmPayment) => + [ + ("redirect", "always"->JSON.Encode.string), + ( + "confirmParams", + [("return_url", sdkHandleConfirmPayment.confirmParams.return_url->JSON.Encode.string)] + ->Dict.fromArray + ->JSON.Encode.object, + ), + ] + ->Dict.fromArray + ->JSON.Encode.object + let achBankDebitBody = ( ~email, ~bank: ACHTypes.data, diff --git a/src/Utilities/Utils.res b/src/Utilities/Utils.res index a5b94ec3..b853d87f 100644 --- a/src/Utilities/Utils.res +++ b/src/Utilities/Utils.res @@ -57,6 +57,26 @@ let getInt = (dict, key, default: int) => { ->Belt.Float.toInt } +let getFloatFromString = (str, default) => { + let val = str->Js.Float.fromString + val->Js.Float.isNaN ? default : val +} + +let getFloatFromJson = (json, default) => { + switch json->Js.Json.classify { + | JSONString(str) => getFloatFromString(str, default) + | JSONNumber(floatValue) => floatValue + | _ => default + } +} + +let getFloat = (dict, key, default) => { + dict + ->Js.Dict.get(key) + ->Belt.Option.map(json => getFloatFromJson(json, default)) + ->Belt.Option.getWithDefault(default) +} + let getJsonBoolValue = (dict, key, default) => { dict->Js.Dict.get(key)->Belt.Option.getWithDefault(default->Js.Json.boolean) } @@ -138,6 +158,10 @@ let getDictFromJson = (json: Js.Json.t) => { json->Js.Json.decodeObject->Belt.Option.getWithDefault(Js.Dict.empty()) } +let getDictfromDict = (dict, key) => { + dict->getJsonObjectFromDict(key)->getDictFromJson +} + let getBool = (dict, key, default) => { getOptionBool(dict, key)->Belt.Option.getWithDefault(default) } diff --git a/src/orca-loader/Elements.res b/src/orca-loader/Elements.res index 6df044b3..75eff009 100644 --- a/src/orca-loader/Elements.res +++ b/src/orca-loader/Elements.res @@ -218,7 +218,6 @@ let make = ( let mountPostMessage = ( mountedIframeRef, selectorString, - sdkHandleConfirmPayment, sdkHandleOneClickConfirmPayment, displaySavedPaymentMethods, ) => { @@ -245,7 +244,6 @@ let make = ( ("publishableKey", publishableKey->Js.Json.string), ("endpoint", endpoint->Js.Json.string), ("sdkSessionId", sdkSessionId->Js.Json.string), - ("sdkHandleConfirmPayment", sdkHandleConfirmPayment->Js.Json.boolean), ("blockConfirm", blockConfirm->Js.Json.boolean), ("switchToCustomPod", switchToCustomPod->Js.Json.boolean), ("endpoint", endpoint->Js.Json.string), diff --git a/src/orca-loader/Hyper.res b/src/orca-loader/Hyper.res index bb037fba..38472a84 100644 --- a/src/orca-loader/Hyper.res +++ b/src/orca-loader/Hyper.res @@ -339,6 +339,17 @@ let make = (publishableKey, options: option, analyticsInfo: option { + let json = event.data->eventToJson + let dict = json->getDictFromJson + switch dict->Js.Dict.get("handleSdkConfirm") { + | Some(payload) => confirmPayment(payload)->ignore + | None => () + } + } + + addSmartEventListener("message", handleSdkConfirm, "handleSdkConfirm") + let elements = elementsOptions => { open Promise let clientSecretId = diff --git a/src/orca-loader/LoaderPaymentElement.res b/src/orca-loader/LoaderPaymentElement.res index e3424295..b28b2dc9 100644 --- a/src/orca-loader/LoaderPaymentElement.res +++ b/src/orca-loader/LoaderPaymentElement.res @@ -19,12 +19,6 @@ let make = (componentType, options, setIframeRef, iframeRef, mountPostMessage) = setIframeRef(ref) } - let sdkHandleConfirmPayment = - options->getDecodedBoolFromJson( - callbackFuncForExtractingValFromDict("sdkHandleConfirmPayment"), - false, - ) - let sdkHandleOneClickConfirmPayment = options->getDecodedBoolFromJson( callbackFuncForExtractingValFromDict("sdkHandleOneClickConfirmPayment"), @@ -170,7 +164,8 @@ let make = (componentType, options, setIframeRef, iframeRef, mountPostMessage) = eventDataObject ->getOptionalJsonFromJson("iframeId") ->getStringfromOptionaljson("no-element") - iframeHeightRef := iframeHeight->getFloatfromjson(200.0) + iframeHeightRef := + iframeHeight->Option.getOr(JSON.Encode.null)->Utils.getFloatFromJson(200.0) if iframeId === localSelectorString { let elem = Window.querySelector( `#orca-payment-element-iframeRef-${localSelectorString}`, @@ -303,7 +298,6 @@ let make = (componentType, options, setIframeRef, iframeRef, mountPostMessage) = mountPostMessage( Window.querySelector(`#orca-payment-element-iframeRef-${localSelectorString}`), localSelectorString, - sdkHandleConfirmPayment, sdkHandleOneClickConfirmPayment, displaySavedPaymentMethods, ) diff --git a/src/orca-loader/OrcaUtils.res b/src/orca-loader/OrcaUtils.res index bbe4aa1c..d0aba433 100644 --- a/src/orca-loader/OrcaUtils.res +++ b/src/orca-loader/OrcaUtils.res @@ -294,10 +294,6 @@ let getBoolfromjson = (json: option, default: bool) => { json->Belt.Option.flatMap(Js.Json.decodeBoolean)->Belt.Option.getWithDefault(default) } -let getFloatfromjson = (json: option, default: float) => { - json->Belt.Option.flatMap(Js.Json.decodeNumber)->Belt.Option.getWithDefault(default) -} - let getStringfromjson = (json: Js.Json.t, default: string) => { json->Js.Json.decodeString->Belt.Option.getWithDefault(default) }