diff --git a/src/Types/HyperLoggerTypes.res b/src/Types/HyperLoggerTypes.res index 18020f94a..52bd1da5c 100644 --- a/src/Types/HyperLoggerTypes.res +++ b/src/Types/HyperLoggerTypes.res @@ -103,6 +103,7 @@ type eventName = | TEST_MODE | PAYMENT_METHOD_ELIGIBILITY_CALL | PAYMENT_METHOD_ELIGIBILITY_CALL_INIT + | DDC_FLOW type maskableDetails = Email | CardDetails type source = Loader | Elements(CardThemeType.mode) | Headless diff --git a/src/Types/PaymentConfirmTypes.res b/src/Types/PaymentConfirmTypes.res index 0fa37776c..1682c098a 100644 --- a/src/Types/PaymentConfirmTypes.res +++ b/src/Types/PaymentConfirmTypes.res @@ -23,18 +23,26 @@ let defaultBacsBankInstruction = { } type bankTransfer = {ach_credit_transfer: achCreditTransfer} -type redirectToUrl = { - returnUrl: string, - url: string, -} type voucherDetails = { download_url: string, reference: string, } +type ddcData = { + iframeUrl: string, + timeoutMs: int, +} + +let defaultDdcData = { + iframeUrl: "", + timeoutMs: 30000, +} + type nextAction = { redirectToUrl: string, + redirectMode: string, + postDdcRedirectUrl: string, popupUrl: string, redirectResponseUrl: string, type_: string, @@ -49,6 +57,7 @@ type nextAction = { display_text: option, border_color: option, iframe_data: option, + ddc_data: option, } type intent = { nextAction: nextAction, @@ -62,12 +71,10 @@ type intent = { } open Utils -let defaultRedirectTourl = { - returnUrl: "", - url: "", -} let defaultNextAction = { redirectToUrl: "", + redirectMode: "required", + postDdcRedirectUrl: "", popupUrl: "", redirectResponseUrl: "", type_: "", @@ -82,6 +89,7 @@ let defaultNextAction = { display_text: None, border_color: None, iframe_data: None, + ddc_data: None, } let defaultIntent = { nextAction: defaultNextAction, @@ -139,6 +147,15 @@ let getVoucherDetails = json => { } } +let getDdcData = json => { + json + ->getOptionalDict("ddc_data") + ->Option.map(ddcDict => { + iframeUrl: ddcDict->getString("iframe_url", ""), + timeoutMs: ddcDict->getInt("timeout_ms", 30000), + }) +} + let getNextAction = (dict, str) => { dict ->Dict.get(str) @@ -146,6 +163,8 @@ let getNextAction = (dict, str) => { ->Option.map(json => { { redirectToUrl: getString(json, "redirect_to_url", ""), + redirectMode: getString(json, "redirect_mode", "required"), + postDdcRedirectUrl: getString(json, "url", ""), popupUrl: getString(json, "popup_url", ""), redirectResponseUrl: getString(json, "redirect_response_url", ""), type_: getString(json, "type", ""), @@ -182,6 +201,7 @@ let getNextAction = (dict, str) => { display_text: json->getOptionString("display_text"), border_color: json->getOptionString("border_color"), iframe_data: Some(json->Utils.getJsonObjectFromDict("iframe_data")), + ddc_data: json->getDdcData, } }) ->Option.getOr(defaultNextAction) diff --git a/src/Utilities/LoggerUtils.res b/src/Utilities/LoggerUtils.res index c4ad63663..0d2886ab5 100644 --- a/src/Utilities/LoggerUtils.res +++ b/src/Utilities/LoggerUtils.res @@ -337,6 +337,7 @@ let apiEventInitMapper = (eventName: HyperLoggerTypes.eventName): option< | AUTHENTICATED_SESSION_INITIATED | ONE_CLICK_HANDLER_CALLBACK | PAYMENT_ELEMENT_OPTIONS - | TEST_MODE => + | TEST_MODE + | DDC_FLOW => None } diff --git a/src/Utilities/NextActionHelpers.res b/src/Utilities/NextActionHelpers.res new file mode 100644 index 000000000..16ac9e10c --- /dev/null +++ b/src/Utilities/NextActionHelpers.res @@ -0,0 +1,147 @@ +open Utils + +let handleDDC = ( + ~ddcData: option, + ~iframeId, + ~isPaymentSession, + ~resolve, + ~data, + ~optLogger, + ~paymentMethod, +) => { + let {iframeUrl, timeoutMs} = ddcData->Option.getOr(PaymentConfirmTypes.defaultDdcData) + + messageParentWindow([ + ("fullscreen", true->JSON.Encode.bool), + ("param", "paymentloader"->JSON.Encode.string), + ("iframeId", iframeId->JSON.Encode.string), + ]) + + let errorType = "confirm_payment_failed" + let errorMessage = "Something went wrong" + + let handleFailure = () => { + closePaymentLoaderIfAny() + if !isPaymentSession { + postFailedSubmitResponse(~errortype=errorType, ~message=errorMessage) + } + let failedSubmitResponse = getFailedSubmitResponse(~errorType, ~message=errorMessage) + resolve(failedSubmitResponse) + } + + if iframeUrl === "" { + LoggerUtils.handleLogging( + ~optLogger, + ~eventName=DDC_FLOW, + ~value="DDC failed: empty iframe URL", + ~paymentMethod, + ~logType=ERROR, + ) + handleFailure() + } else { + let timeoutIdRef = ref(None) + let messageHandlerRef = ref(None) + let iframeRef = ref(None) + + let cleanup = () => { + timeoutIdRef.contents->Option.forEach(clearTimeout) + messageHandlerRef.contents->Option.forEach(h => Window.removeEventListener("message", h)) + iframeRef.contents->Option.forEach(Window.remove) + timeoutIdRef := None + messageHandlerRef := None + iframeRef := None + } + + let handleRedirectToUrl = (redirectUrl, redirectMode) => { + closePaymentLoaderIfAny() + switch redirectMode { + | "if_required" => + if !isPaymentSession { + messageParentWindow([("openurl_if_required", redirectUrl->JSON.Encode.string)]) + } else { + resolve(data) + } + | _ => { + LoggerUtils.handleLogging( + ~optLogger, + ~eventName=REDIRECTING_USER, + ~value="Post DDC redirection url : " ++ redirectUrl, + ~paymentMethod, + ~logType=INFO, + ) + openUrl(redirectUrl) + } + } + } + + let handleMessage = (ev: Window.event) => { + try { + let json = ev.data->Identity.anyTypeToJson + let dict = json->getDictFromJson + + if dict->Dict.get("next_action")->Option.isSome { + let nextAction = PaymentConfirmTypes.getNextAction(dict, "next_action") + let nextActionType = nextAction.type_ + let redirectUrl = nextAction.postDdcRedirectUrl + let redirectMode = nextAction.redirectMode + cleanup() + if nextActionType === "redirect_to_url" && redirectUrl !== "" { + LoggerUtils.handleLogging( + ~optLogger, + ~eventName=DDC_FLOW, + ~value="DDC completed successfully", + ~paymentMethod, + ) + handleRedirectToUrl(redirectUrl, redirectMode) + } else { + LoggerUtils.handleLogging( + ~optLogger, + ~eventName=DDC_FLOW, + ~value="DDC failed: invalid next action type - " ++ nextActionType, + ~paymentMethod, + ~logType=ERROR, + ) + handleFailure() + } + } + } catch { + | exn => + let err = exn->Identity.anyTypeToJson->JSON.stringify + LoggerUtils.handleLogging( + ~optLogger, + ~eventName=DDC_FLOW, + ~value="DDC failed: message parse error - " ++ err, + ~paymentMethod, + ~logType=ERROR, + ) + cleanup() + handleFailure() + } + } + + messageHandlerRef := Some(handleMessage) + Window.addEventListener("message", handleMessage) + + LoggerUtils.handleLogging( + ~optLogger, + ~eventName=DDC_FLOW, + ~value="DDC initiated - iframe URL: " ++ iframeUrl, + ~paymentMethod, + ) + + let iframe = Window.body->makeHiddenIframe(~src=iframeUrl, ~id="ddc-iframe") + iframeRef := Some(iframe) + + timeoutIdRef := Some(setTimeout(() => { + LoggerUtils.handleLogging( + ~optLogger, + ~eventName=DDC_FLOW, + ~value="DDC timed out", + ~paymentMethod, + ~logType=ERROR, + ) + cleanup() + handleFailure() + }, timeoutMs)) + } +} diff --git a/src/Utilities/PaymentHelpers.res b/src/Utilities/PaymentHelpers.res index 4806893c8..927e24cff 100644 --- a/src/Utilities/PaymentHelpers.res +++ b/src/Utilities/PaymentHelpers.res @@ -17,8 +17,6 @@ let getPaymentType = paymentMethodType => | _ => Other } -let closePaymentLoaderIfAny = () => messageParentWindow([("fullscreen", false->JSON.Encode.bool)]) - let retrievePaymentIntent = async ( clientSecret, ~headers=?, @@ -759,6 +757,16 @@ let rec intentCall = ( ("iframeId", iframeId->JSON.Encode.string), ("metadata", metaData->JSON.Encode.object), ]) + } else if intent.nextAction.type_ === "invoke_ddc" { + NextActionHelpers.handleDDC( + ~ddcData=intent.nextAction.ddc_data, + ~iframeId, + ~isPaymentSession, + ~resolve, + ~data, + ~optLogger, + ~paymentMethod, + ) } else if intent.nextAction.type_ === "display_voucher_information" { let voucherData = intent.nextAction.voucher_details->Option.getOr({ download_url: "", diff --git a/src/Utilities/PaymentHelpersV2.res b/src/Utilities/PaymentHelpersV2.res index dd0f035a7..12bd4b4ab 100644 --- a/src/Utilities/PaymentHelpersV2.res +++ b/src/Utilities/PaymentHelpersV2.res @@ -76,7 +76,7 @@ let intentCall = ( let dict = data->getDictFromJson let errorObj = PaymentError.itemToObjMapper(dict) if !isPaymentSession { - PaymentHelpers.closePaymentLoaderIfAny() + closePaymentLoaderIfAny() postFailedSubmitResponse( ~errortype=errorObj.error.type_, ~message=errorObj.error.message, @@ -99,7 +99,7 @@ let intentCall = ( (resolve, _) => { let _exceptionMessage = err->formatException if !isPaymentSession { - PaymentHelpers.closePaymentLoaderIfAny() + closePaymentLoaderIfAny() postFailedSubmitResponse(~errortype="server_error", ~message="Something went wrong") } if handleUserError { @@ -140,7 +140,7 @@ let intentCall = ( if isCallbackUsedVal->Option.getOr(false) { handleOnCompleteDoThisMessage() } else { - PaymentHelpers.closePaymentLoaderIfAny() + closePaymentLoaderIfAny() } postSubmitResponse(~jsonData=data, ~url=url.href) @@ -155,7 +155,7 @@ let intentCall = ( } | _ => if isCallbackUsedVal->Option.getOr(false) { - PaymentHelpers.closePaymentLoaderIfAny() + closePaymentLoaderIfAny() handleOnCompleteDoThisMessage() } else { handleOpenUrl(url.href) @@ -240,7 +240,7 @@ let intentCall = ( let _exceptionMessage = err->formatException if !isPaymentSession { - PaymentHelpers.closePaymentLoaderIfAny() + closePaymentLoaderIfAny() postFailedSubmitResponse(~errortype="server_error", ~message="Something went wrong") } if handleUserError { diff --git a/src/Utilities/Utils.res b/src/Utilities/Utils.res index 59402bb0d..2f6e2528c 100644 --- a/src/Utilities/Utils.res +++ b/src/Utilities/Utils.res @@ -185,6 +185,10 @@ let getDictFromObj = (dict, key) => { dict->Dict.get(key)->Option.flatMap(JSON.Decode.object)->Option.getOr(Dict.make()) } +let getOptionalDict = (dict, key) => { + dict->Dict.get(key)->Option.flatMap(JSON.Decode.object) +} + let getJsonObjectFromDict = (dict, key) => { dict->Dict.get(key)->Option.getOr(JSON.Encode.object(Dict.make())) } @@ -1482,6 +1486,17 @@ let makeIframe = (element, url) => { element->appendChild(iframe) }) } +let makeHiddenIframe = (element, ~src, ~id) => { + let iframe = Window.createElement("iframe") + iframe->Window.setAttribute("id", id) + iframe->Window.setAttribute("src", src) + iframe->Window.setAttribute( + "style", + "position: absolute; width: 1px; height: 1px; border: none; overflow: hidden; left: -9999px; top: -9999px;", + ) + element->Window.appendChild(iframe) + iframe +} let makeForm = (element, url, id) => { open Types let form = createElement("form") @@ -1695,6 +1710,8 @@ let handleFailureResponse = (~message, ~errorType) => ), ]->getJsonFromArrayOfJson +let closePaymentLoaderIfAny = () => messageParentWindow([("fullscreen", false->JSON.Encode.bool)]) + let getPaymentId = clientSecret => String.split(clientSecret, "_secret_")->Array.get(0)->Option.getOr("") diff --git a/src/hyper-log-catcher/HyperLogger.res b/src/hyper-log-catcher/HyperLogger.res index 4c0cfa3c0..540f0ac97 100644 --- a/src/hyper-log-catcher/HyperLogger.res +++ b/src/hyper-log-catcher/HyperLogger.res @@ -232,6 +232,7 @@ let make = (~sessionId=?, ~source: source, ~clientSecret=?, ~merchantId=?, ~meta APPLE_PAY_FLOW, PLAID_SDK, NETWORK_STATE, + DDC_FLOW, ] arrayOfLogs ->Array.find(log => {