From 67be317957ff4d267f20cf5b376277116b7e4861 Mon Sep 17 00:00:00 2001 From: ArushKapoorJuspay <121166031+ArushKapoorJuspay@users.noreply.github.com> Date: Tue, 12 Mar 2024 02:43:56 +0530 Subject: [PATCH] feat: Added Payment Session Headless (#209) Co-authored-by: Praful Koppalkar <126236898+prafulkoppalkar@users.noreply.github.com> --- src/Utilities/PaymentHelpers.res | 714 ++++++++++++---------- src/Utilities/Utils.res | 13 + src/Window.res | 3 + src/orca-loader/Hyper.res | 29 + src/orca-loader/HyperLoader.res | 1 + src/orca-loader/LoaderPaymentElement.res | 5 +- src/orca-loader/PaymentSession.res | 26 + src/orca-loader/PaymentSessionMethods.res | 180 ++++++ src/orca-loader/Types.res | 30 + src/orca-log-catcher/OrcaLogger.res | 2 + 10 files changed, 673 insertions(+), 330 deletions(-) create mode 100644 src/orca-loader/PaymentSession.res create mode 100644 src/orca-loader/PaymentSessionMethods.res diff --git a/src/Utilities/PaymentHelpers.res b/src/Utilities/PaymentHelpers.res index b99cc78fc..812dcd0d1 100644 --- a/src/Utilities/PaymentHelpers.res +++ b/src/Utilities/PaymentHelpers.res @@ -115,6 +115,9 @@ let rec intentCall = ( ~switchToCustomPod, ~sdkHandleOneClickConfirmPayment, ~counter, + ~isPaymentSession=false, + ~paymentSessionRedirect="if_redirect", + (), ) => { open Promise let isConfirm = uri->Js.String2.includes("/confirm") @@ -130,6 +133,13 @@ let rec intentCall = ( ~logCategory=API, (), ) + let handleOpenUrl = url => { + if isPaymentSession { + Window.location.replace(. url) + } else { + openUrl(url) + } + } fetchApi( uri, ~method_=fetchMethod, @@ -147,355 +157,405 @@ let rec intentCall = ( res ->Fetch.Response.json ->then(data => { - if isConfirm { - let paymentMethod = switch paymentType { - | Card => "CARD" - | _ => - bodyStr - ->Js.Json.parseExn - ->Utils.getDictFromJson - ->Utils.getString("payment_method_type", "") - } - handleLogging( - ~optLogger, - ~value=data->Js.Json.stringify, - ~eventName=PAYMENT_FAILED, - ~paymentMethod, - (), - ) - } - logApi( - ~optLogger, - ~url=uri, - ~data, - ~statusCode, - ~type_="err", - ~eventName, - ~logType=ERROR, - ~logCategory=API, - (), - ) - - let dict = data->getDictFromJson - let errorObj = PaymentError.itemToObjMapper(dict) - closePaymentLoaderIfAny() - postFailedSubmitResponse(~errortype=errorObj.error.type_, ~message=errorObj.error.message) - if handleUserError { - openUrl(url.href) - } - resolve() - }) - ->catch(err => { - let exceptionMessage = err->Utils.formatException - logApi( - ~optLogger, - ~url=uri, - ~statusCode, - ~type_="no_response", - ~data=exceptionMessage, - ~eventName, - ~logType=ERROR, - ~logCategory=API, - (), - ) - if counter >= 5 { - closePaymentLoaderIfAny() - postFailedSubmitResponse(~errortype="server_error", ~message="Something went wrong") - if handleUserError { - openUrl(url.href) - } - } else { - let paymentIntentID = - Js.String2.split(clientSecret, "_secret_") - ->Belt.Array.get(0) - ->Belt.Option.getWithDefault("") - let endpoint = ApiEndpoint.getApiEndPoint(~publishableKey=confirmParam.publishableKey, ()) - let retrieveUri = `${endpoint}/payments/${paymentIntentID}?client_secret=${clientSecret}` - intentCall( - ~fetchApi, - ~uri=retrieveUri, - ~headers, - ~bodyStr, - ~confirmParam: ConfirmType.confirmParams, - ~clientSecret, - ~optLogger, - ~handleUserError, - ~paymentType, - ~iframeId, - ~fetchMethod=Get, - ~setIsManualRetryEnabled, - ~switchToCustomPod, - ~sdkHandleOneClickConfirmPayment, - ~counter=counter + 1, - ) - } - resolve() - }) - ->ignore - } else { - res - ->Fetch.Response.json - ->then(data => { - logApi(~optLogger, ~url=uri, ~statusCode, ~type_="response", ~eventName, ()) - let intent = PaymentConfirmTypes.itemToObjMapper(data->getDictFromJson) - let paymentMethod = switch paymentType { - | Card => "CARD" - | _ => intent.payment_method_type - } - - let url = urlSearch(confirmParam.return_url) - url.searchParams.set(. "payment_intent_client_secret", clientSecret) - url.searchParams.set(. "status", intent.status) - if intent.status == "requires_customer_action" { - if intent.nextAction.type_ == "redirect_to_url" { - handleLogging( - ~optLogger, - ~value="", - ~internalMetadata=intent.nextAction.redirectToUrl, - ~eventName=REDIRECTING_USER, - ~paymentMethod, - (), - ) - openUrl(intent.nextAction.redirectToUrl) - } else if intent.nextAction.type_ == "display_bank_transfer_information" { - let metadata = switch intent.nextAction.bank_transfer_steps_and_charges_details { - | Some(obj) => obj->Utils.getDictFromJson - | None => Js.Dict.empty() + Js.Promise.make( + (~resolve, ~reject as _) => { + if isConfirm { + let paymentMethod = switch paymentType { + | Card => "CARD" + | _ => + bodyStr + ->Js.Json.parseExn + ->Utils.getDictFromJson + ->Utils.getString("payment_method_type", "") + } + handleLogging( + ~optLogger, + ~value=data->Js.Json.stringify, + ~eventName=PAYMENT_FAILED, + ~paymentMethod, + (), + ) } - let dict = deepCopyDict(metadata) - dict->Js.Dict.set("data", data) - dict->Js.Dict.set("url", url.href->Js.Json.string) - handleLogging( + logApi( ~optLogger, - ~value="", - ~internalMetadata=dict->Js.Json.object_->Js.Json.stringify, - ~eventName=DISPLAY_BANK_TRANSFER_INFO_PAGE, - ~paymentMethod, + ~url=uri, + ~data, + ~statusCode, + ~type_="err", + ~eventName, + ~logType=ERROR, + ~logCategory=API, (), ) - handlePostMessage([ - ("fullscreen", true->Js.Json.boolean), - ("param", `${intent.payment_method_type}BankTransfer`->Js.Json.string), - ("iframeId", iframeId->Js.Json.string), - ("metadata", dict->Js.Json.object_), - ]) - } else if intent.nextAction.type_ === "qr_code_information" { - let qrData = intent.nextAction.image_data_url->Belt.Option.getWithDefault("") - let expiryTime = intent.nextAction.display_to_timestamp->Belt.Option.getWithDefault(0.0) - 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 = - [ - ("qrData", qrData->Js.Json.string), - ("paymentIntentId", clientSecret->Js.Json.string), - ("headers", headerObj->Js.Json.object_), - ("expiryTime", expiryTime->Belt.Float.toString->Js.Json.string), - ("url", url.href->Js.Json.string), - ]->Js.Dict.fromArray - handleLogging( - ~optLogger, - ~value="", - ~internalMetadata=metaData->Js.Json.object_->Js.Json.stringify, - ~eventName=DISPLAY_QR_CODE_INFO_PAGE, - ~paymentMethod, - (), - ) - handlePostMessage([ - ("fullscreen", true->Js.Json.boolean), - ("param", `qrData`->Js.Json.string), - ("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 - | None => Js.Dict.empty() + + let dict = data->getDictFromJson + let errorObj = PaymentError.itemToObjMapper(dict) + if !isPaymentSession { + closePaymentLoaderIfAny() + postFailedSubmitResponse( + ~errortype=errorObj.error.type_, + ~message=errorObj.error.message, + ) } - let walletName = session_token->Utils.getString("wallet_name", "") - let message = switch walletName { - | "apple_pay" => [ - ("applePayButtonClicked", true->Js.Json.boolean), - ("applePayPresent", session_token->toJson), - ] - | "google_pay" => [("googlePayThirdPartyFlow", session_token->toJson)] - | _ => [] + if handleUserError { + handleOpenUrl(url.href) + } else { + let failedSubmitResponse = getFailedSubmitResponse( + ~errorType=errorObj.error.type_, + ~message=errorObj.error.message, + ) + resolve(. failedSubmitResponse) } - - handlePostMessage(message) - } else { - postFailedSubmitResponse( - ~errortype="confirm_payment_failed", - ~message="Payment failed. Try again!", + }, + )->then(resolve) + }) + ->catch(err => { + Js.Promise.make( + (~resolve, ~reject as _) => { + let exceptionMessage = err->Utils.formatException + logApi( + ~optLogger, + ~url=uri, + ~statusCode, + ~type_="no_response", + ~data=exceptionMessage, + ~eventName, + ~logType=ERROR, + ~logCategory=API, + (), ) - if uri->Js.String2.includes("force_sync=true") { - handleLogging( + if counter >= 5 { + if !isPaymentSession { + closePaymentLoaderIfAny() + postFailedSubmitResponse(~errortype="server_error", ~message="Something went wrong") + } + if handleUserError { + handleOpenUrl(url.href) + } else { + let failedSubmitResponse = getFailedSubmitResponse( + ~errorType="server_error", + ~message="Something went wrong", + ) + resolve(. failedSubmitResponse) + } + } else { + let paymentIntentID = + Js.String2.split(clientSecret, "_secret_") + ->Belt.Array.get(0) + ->Belt.Option.getWithDefault("") + let endpoint = ApiEndpoint.getApiEndPoint( + ~publishableKey=confirmParam.publishableKey, + (), + ) + let retrieveUri = `${endpoint}/payments/${paymentIntentID}?client_secret=${clientSecret}` + intentCall( + ~fetchApi, + ~uri=retrieveUri, + ~headers, + ~bodyStr, + ~confirmParam: ConfirmType.confirmParams, + ~clientSecret, ~optLogger, - ~value=intent.nextAction.type_, - ~internalMetadata=intent.nextAction.type_, - ~eventName=REDIRECTING_USER, - ~paymentMethod, - ~logType=ERROR, + ~handleUserError, + ~paymentType, + ~iframeId, + ~fetchMethod=Get, + ~setIsManualRetryEnabled, + ~switchToCustomPod, + ~sdkHandleOneClickConfirmPayment, + ~counter=counter + 1, (), ) - openUrl(url.href) + ->then( + res => { + resolve(. res) + Promise.resolve() + }, + ) + ->ignore } - } - } else if intent.status == "processing" { - if intent.nextAction.type_ == "third_party_sdk_session_token" { - let session_token = switch intent.nextAction.session_token { - | Some(token) => token->Utils.getDictFromJson - | None => Js.Dict.empty() + }, + )->then(resolve) + }) + } else { + res + ->Fetch.Response.json + ->then(data => { + Js.Promise.make( + (~resolve, ~reject as _) => { + logApi(~optLogger, ~url=uri, ~statusCode, ~type_="response", ~eventName, ()) + let intent = PaymentConfirmTypes.itemToObjMapper(data->getDictFromJson) + let paymentMethod = switch paymentType { + | Card => "CARD" + | _ => intent.payment_method_type } - let walletName = session_token->Utils.getString("wallet_name", "") - let message = switch walletName { - | "apple_pay" => [ - ("applePayButtonClicked", true->Js.Json.boolean), - ("applePayPresent", session_token->toJson), - ] - | "google_pay" => [("googlePayThirdPartyFlow", session_token->toJson)] - | _ => [] + + let url = urlSearch(confirmParam.return_url) + url.searchParams.set(. "payment_intent_client_secret", clientSecret) + url.searchParams.set(. "status", intent.status) + + let handleProcessingStatus = (paymentType, sdkHandleOneClickConfirmPayment) => { + switch (paymentType, sdkHandleOneClickConfirmPayment) { + | (Card, _) + | (Gpay, false) + | (Applepay, false) + | (Paypal, false) => + if !isPaymentSession { + postSubmitResponse(~jsonData=data, ~url=url.href) + } else if paymentSessionRedirect === "always" { + handleOpenUrl(url.href) + } else { + resolve(. data) + } + | _ => handleOpenUrl(url.href) + } } - handlePostMessage(message) - } else { - switch (paymentType, sdkHandleOneClickConfirmPayment) { - | (Card, _) - | (Gpay, false) - | (Applepay, false) - | (Paypal, false) => - postSubmitResponse(~jsonData=data, ~url=url.href) - | _ => openUrl(url.href) + if intent.status == "requires_customer_action" { + if intent.nextAction.type_ == "redirect_to_url" { + handleLogging( + ~optLogger, + ~value="", + ~internalMetadata=intent.nextAction.redirectToUrl, + ~eventName=REDIRECTING_USER, + ~paymentMethod, + (), + ) + handleOpenUrl(intent.nextAction.redirectToUrl) + } else if intent.nextAction.type_ == "display_bank_transfer_information" { + let metadata = switch intent.nextAction.bank_transfer_steps_and_charges_details { + | Some(obj) => obj->Utils.getDictFromJson + | None => Js.Dict.empty() + } + let dict = deepCopyDict(metadata) + dict->Js.Dict.set("data", data) + dict->Js.Dict.set("url", url.href->Js.Json.string) + handleLogging( + ~optLogger, + ~value="", + ~internalMetadata=dict->Js.Json.object_->Js.Json.stringify, + ~eventName=DISPLAY_BANK_TRANSFER_INFO_PAGE, + ~paymentMethod, + (), + ) + if !isPaymentSession { + handlePostMessage([ + ("fullscreen", true->Js.Json.boolean), + ("param", `${intent.payment_method_type}BankTransfer`->Js.Json.string), + ("iframeId", iframeId->Js.Json.string), + ("metadata", dict->Js.Json.object_), + ]) + } + resolve(. data) + } else if intent.nextAction.type_ === "qr_code_information" { + let qrData = intent.nextAction.image_data_url->Belt.Option.getWithDefault("") + let expiryTime = + intent.nextAction.display_to_timestamp->Belt.Option.getWithDefault(0.0) + 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 = + [ + ("qrData", qrData->Js.Json.string), + ("paymentIntentId", clientSecret->Js.Json.string), + ("headers", headerObj->Js.Json.object_), + ("expiryTime", expiryTime->Belt.Float.toString->Js.Json.string), + ("url", url.href->Js.Json.string), + ]->Js.Dict.fromArray + handleLogging( + ~optLogger, + ~value="", + ~internalMetadata=metaData->Js.Json.object_->Js.Json.stringify, + ~eventName=DISPLAY_QR_CODE_INFO_PAGE, + ~paymentMethod, + (), + ) + if !isPaymentSession { + handlePostMessage([ + ("fullscreen", true->Js.Json.boolean), + ("param", `qrData`->Js.Json.string), + ("iframeId", iframeId->Js.Json.string), + ("metadata", metaData->Js.Json.object_), + ]) + } + resolve(. data) + } else if intent.nextAction.type_ == "third_party_sdk_session_token" { + let session_token = switch intent.nextAction.session_token { + | Some(token) => token->Utils.getDictFromJson + | None => Js.Dict.empty() + } + let walletName = session_token->Utils.getString("wallet_name", "") + let message = switch walletName { + | "apple_pay" => [ + ("applePayButtonClicked", true->Js.Json.boolean), + ("applePayPresent", session_token->toJson), + ] + | "google_pay" => [("googlePayThirdPartyFlow", session_token->toJson)] + | _ => [] + } + + if !isPaymentSession { + handlePostMessage(message) + } + resolve(. data) + } else { + if !isPaymentSession { + postFailedSubmitResponse( + ~errortype="confirm_payment_failed", + ~message="Payment failed. Try again!", + ) + } + if uri->Js.String2.includes("force_sync=true") { + handleLogging( + ~optLogger, + ~value=intent.nextAction.type_, + ~internalMetadata=intent.nextAction.type_, + ~eventName=REDIRECTING_USER, + ~paymentMethod, + ~logType=ERROR, + (), + ) + handleOpenUrl(url.href) + } else { + let failedSubmitResponse = getFailedSubmitResponse( + ~errorType="confirm_payment_failed", + ~message="Payment failed. Try again!", + ) + resolve(. failedSubmitResponse) + } + } + } else if intent.status == "processing" { + if intent.nextAction.type_ == "third_party_sdk_session_token" { + let session_token = switch intent.nextAction.session_token { + | Some(token) => token->Utils.getDictFromJson + | None => Js.Dict.empty() + } + let walletName = session_token->Utils.getString("wallet_name", "") + let message = switch walletName { + | "apple_pay" => [ + ("applePayButtonClicked", true->Js.Json.boolean), + ("applePayPresent", session_token->toJson), + ] + | "google_pay" => [("googlePayThirdPartyFlow", session_token->toJson)] + | _ => [] + } + + if !isPaymentSession { + handlePostMessage(message) + } + } else { + handleProcessingStatus(paymentType, sdkHandleOneClickConfirmPayment) + } + resolve(. data) + } else if intent.status != "" { + if intent.status === "succeeded" { + handleLogging( + ~optLogger, + ~value=intent.status, + ~eventName=PAYMENT_SUCCESS, + ~paymentMethod, + (), + ) + } else if intent.status === "failed" { + handleLogging( + ~optLogger, + ~value=intent.status, + ~eventName=PAYMENT_FAILED, + ~paymentMethod, + (), + ) + } + if intent.status === "failed" { + setIsManualRetryEnabled(. _ => intent.manualRetryAllowed) + } + handleProcessingStatus(paymentType, sdkHandleOneClickConfirmPayment) + } else if !isPaymentSession { + postFailedSubmitResponse( + ~errortype="confirm_payment_failed", + ~message="Payment failed. Try again!", + ) + } else { + let failedSubmitResponse = getFailedSubmitResponse( + ~errorType="confirm_payment_failed", + ~message="Payment failed. Try again!", + ) + resolve(. failedSubmitResponse) } - } - } else if intent.status != "" { - if intent.status === "succeeded" { - handleLogging( - ~optLogger, - ~value=intent.status, - ~eventName=PAYMENT_SUCCESS, - ~paymentMethod, - (), - ) - } else if intent.status === "failed" { - handleLogging( - ~optLogger, - ~value=intent.status, - ~eventName=PAYMENT_FAILED, - ~paymentMethod, - (), - ) - } - if intent.status === "failed" { - setIsManualRetryEnabled(. _ => intent.manualRetryAllowed) - } - switch (paymentType, sdkHandleOneClickConfirmPayment) { - | (Card, _) - | (Gpay, false) - | (Applepay, false) - | (Paypal, false) => - postSubmitResponse(~jsonData=data, ~url=url.href) - | _ => openUrl(url.href) - } - } else { - postFailedSubmitResponse( - ~errortype="confirm_payment_failed", - ~message="Payment failed. Try again!", - ) - } - resolve() + }, + )->then(resolve) }) - ->ignore } - resolve() }) ->catch(err => { - let exceptionMessage = err->Utils.formatException - logApi( - ~optLogger, - ~url=uri, - ~eventName, - ~type_="no_response", - ~data=exceptionMessage, - ~logType=ERROR, - ~logCategory=API, - (), - ) - if counter >= 5 { + Js.Promise.make((~resolve, ~reject as _) => { let url = urlSearch(confirmParam.return_url) url.searchParams.set(. "payment_intent_client_secret", clientSecret) url.searchParams.set(. "status", "failed") - closePaymentLoaderIfAny() - postFailedSubmitResponse(~errortype="server_error", ~message="Something went wrong") - if handleUserError { - openUrl(url.href) - } - } else { - let paymentIntentID = - Js.String2.split(clientSecret, "_secret_") - ->Belt.Array.get(0) - ->Belt.Option.getWithDefault("") - let endpoint = ApiEndpoint.getApiEndPoint(~publishableKey=confirmParam.publishableKey, ()) - let retrieveUri = `${endpoint}/payments/${paymentIntentID}?client_secret=${clientSecret}` - intentCall( - ~fetchApi, - ~uri=retrieveUri, - ~headers, - ~bodyStr, - ~confirmParam: ConfirmType.confirmParams, - ~clientSecret, + let exceptionMessage = err->Utils.formatException + logApi( ~optLogger, - ~handleUserError, - ~paymentType, - ~iframeId, - ~fetchMethod=Get, - ~setIsManualRetryEnabled, - ~switchToCustomPod, - ~sdkHandleOneClickConfirmPayment, - ~counter=counter + 1, + ~url=uri, + ~eventName, + ~type_="no_response", + ~data=exceptionMessage, + ~logType=ERROR, + ~logCategory=API, + (), ) - } - resolve() + if counter >= 5 { + if !isPaymentSession { + closePaymentLoaderIfAny() + postFailedSubmitResponse(~errortype="server_error", ~message="Something went wrong") + } + if handleUserError { + handleOpenUrl(url.href) + } else { + let failedSubmitResponse = getFailedSubmitResponse( + ~errorType="server_error", + ~message="Something went wrong", + ) + resolve(. failedSubmitResponse) + } + } else { + let paymentIntentID = + Js.String2.split(clientSecret, "_secret_") + ->Belt.Array.get(0) + ->Belt.Option.getWithDefault("") + let endpoint = ApiEndpoint.getApiEndPoint(~publishableKey=confirmParam.publishableKey, ()) + let retrieveUri = `${endpoint}/payments/${paymentIntentID}?client_secret=${clientSecret}` + intentCall( + ~fetchApi, + ~uri=retrieveUri, + ~headers, + ~bodyStr, + ~confirmParam: ConfirmType.confirmParams, + ~clientSecret, + ~optLogger, + ~handleUserError, + ~paymentType, + ~iframeId, + ~fetchMethod=Get, + ~setIsManualRetryEnabled, + ~switchToCustomPod, + ~sdkHandleOneClickConfirmPayment, + ~counter=counter + 1, + ~isPaymentSession, + (), + ) + ->then( + res => { + resolve(. res) + Promise.resolve() + }, + ) + ->ignore + } + })->then(resolve) }) - ->ignore } let usePaymentSync = (optLogger: option, paymentType: payment) => { @@ -528,7 +588,8 @@ let usePaymentSync = (optLogger: option, paymentType: pay ~switchToCustomPod, ~sdkHandleOneClickConfirmPayment=keys.sdkHandleOneClickConfirmPayment, ~counter=0, - ) + (), + )->ignore } switch list { | Loaded(_) => paymentSync() @@ -679,7 +740,8 @@ let usePaymentIntent = (optLogger: option, paymentType: p ~switchToCustomPod, ~sdkHandleOneClickConfirmPayment=keys.sdkHandleOneClickConfirmPayment, ~counter=0, - ) + (), + )->ignore } } diff --git a/src/Utilities/Utils.res b/src/Utilities/Utils.res index b853d87f1..89951da92 100644 --- a/src/Utilities/Utils.res +++ b/src/Utilities/Utils.res @@ -305,6 +305,19 @@ let postSubmitResponse = (~jsonData, ~url) => { ]) } +let getFailedSubmitResponse = (~errorType, ~message) => { + [ + ( + "error", + [("type", errorType->Js.Json.string), ("message", message->Js.Json.string)] + ->Js.Dict.fromArray + ->Js.Json.object_, + ), + ] + ->Js.Dict.fromArray + ->Js.Json.object_ +} + let toCamelCase = str => { if str->Js.String2.includes(":") { str diff --git a/src/Window.res b/src/Window.res index 4b327b81c..c96077da3 100644 --- a/src/Window.res +++ b/src/Window.res @@ -125,3 +125,6 @@ let isSandbox = hostname === "beta.hyperswitch.io" let isInteg = hostname === "dev.hyperswitch.io" let isProd = hostname === "checkout.hyperswitch.io" + +type location = {replace: (. string) => unit} +@val @scope("window") external location: location = "location" diff --git a/src/orca-loader/Hyper.res b/src/orca-loader/Hyper.res index 38472a849..dc17c35e4 100644 --- a/src/orca-loader/Hyper.res +++ b/src/orca-loader/Hyper.res @@ -499,6 +499,34 @@ let make = (publishableKey, options: option, analyticsInfo: option { + open Promise + + let clientSecretId = + paymentSessionOptions + ->Js.Json.decodeObject + ->Belt.Option.flatMap(x => x->Js.Dict.get("clientSecret")) + ->Belt.Option.flatMap(Js.Json.decodeString) + ->Belt.Option.getWithDefault("") + clientSecret := clientSecretId + Js.Promise.make((~resolve, ~reject as _) => { + logger.setClientSecret(clientSecretId) + resolve(. Js.Json.null) + }) + ->then(_ => { + logger.setLogInfo(~value=Window.href, ~eventName=PAYMENT_SESSION_INITIATED, ()) + resolve() + }) + ->ignore + + PaymentSession.make( + paymentSessionOptions, + ~clientSecret={clientSecretId}, + ~publishableKey, + ~logger=Some(logger), + ) + } + let returnObject = { confirmOneClickPayment, confirmPayment, @@ -507,6 +535,7 @@ let make = (publishableKey, options: option, analyticsInfo: option { @val external window: {..} = "window" window["Hyper"] = Hyper.make +window["Hyper"]["init"] = Hyper.make let isWordpress = window["wp"] !== Js.Json.null if !isWordpress { diff --git a/src/orca-loader/LoaderPaymentElement.res b/src/orca-loader/LoaderPaymentElement.res index b28b2dc96..b618f27fc 100644 --- a/src/orca-loader/LoaderPaymentElement.res +++ b/src/orca-loader/LoaderPaymentElement.res @@ -6,9 +6,6 @@ open OrcaUtils external eventToJson: Types.eventData => Js.Json.t = "%identity" -type location = {replace: (. string) => unit} -@val @scope("window") external location: location = "location" - @val @scope(("navigator", "clipboard")) external writeText: string => Js.Promise.t<'a> = "writeText" @@ -188,7 +185,7 @@ let make = (componentType, options, setIframeRef, iframeRef, mountPostMessage) = switch eventDataObject->getOptionalJsonFromJson("openurl") { | Some(val) => { let url = val->getStringfromjson("") - location.replace(. url) + Window.location.replace(. url) } | None => () } diff --git a/src/orca-loader/PaymentSession.res b/src/orca-loader/PaymentSession.res new file mode 100644 index 000000000..87d485baf --- /dev/null +++ b/src/orca-loader/PaymentSession.res @@ -0,0 +1,26 @@ +open Types + +let make = (options, ~clientSecret, ~publishableKey, ~logger: option) => { + let logger = logger->Belt.Option.getWithDefault(OrcaLogger.defaultLoggerConfig) + let switchToCustomPod = + GlobalVars.isInteg && + options + ->Js.Json.decodeObject + ->Belt.Option.flatMap(x => x->Js.Dict.get("switchToCustomPod")) + ->Belt.Option.flatMap(Js.Json.decodeBoolean) + ->Belt.Option.getWithDefault(false) + let endpoint = ApiEndpoint.getApiEndPoint(~publishableKey, ()) + + let defaultInitPaymentSession = { + getCustomerSavedPaymentMethods: _ => + PaymentSessionMethods.getCustomerSavedPaymentMethods( + ~clientSecret, + ~publishableKey, + ~endpoint, + ~logger, + ~switchToCustomPod, + ), + } + + defaultInitPaymentSession +} diff --git a/src/orca-loader/PaymentSessionMethods.res b/src/orca-loader/PaymentSessionMethods.res new file mode 100644 index 000000000..dbb085fb7 --- /dev/null +++ b/src/orca-loader/PaymentSessionMethods.res @@ -0,0 +1,180 @@ +open Types + +external customerSavedPaymentMethodsToJson: getCustomerSavedPaymentMethods => Js.Json.t = + "%identity" + +let getCustomerSavedPaymentMethods = ( + ~clientSecret, + ~publishableKey, + ~endpoint, + ~logger, + ~switchToCustomPod, +) => { + open Promise + PaymentHelpers.useCustomerDetails( + ~clientSecret, + ~publishableKey, + ~endpoint, + ~switchToCustomPod, + ~optLogger=Some(logger), + ) + ->then(customerDetails => { + let customerPaymentMethods = + customerDetails + ->Js.Json.decodeObject + ->Belt.Option.flatMap(x => x->Js.Dict.get("customer_payment_methods")) + ->Belt.Option.flatMap(Js.Json.decodeArray) + ->Belt.Option.getWithDefault([]) + ->Js.Array2.filter(customerPaymentMethod => { + customerPaymentMethod + ->Js.Json.decodeObject + ->Belt.Option.flatMap(x => x->Js.Dict.get("default_payment_method_set")) + ->Belt.Option.flatMap(Js.Json.decodeBoolean) + ->Belt.Option.getWithDefault(false) + }) + + switch customerPaymentMethods->Belt.Array.get(0) { + | Some(customerDefaultPaymentMethod) => + let getCustomerDefaultSavedPaymentMethodData = () => { + customerDefaultPaymentMethod + } + + let confirmWithCustomerDefaultPaymentMethod = payload => { + let customerPaymentMethod = + customerDefaultPaymentMethod + ->Js.Json.decodeObject + ->Belt.Option.getWithDefault(Js.Dict.empty()) + let paymentToken = + customerPaymentMethod->Utils.getJsonFromDict("payment_token", Js.Json.null) + let paymentMethod = + customerPaymentMethod->Utils.getJsonFromDict("payment_method", Js.Json.null) + let paymentMethodType = + customerPaymentMethod->Utils.getJsonFromDict("payment_method_type", Js.Json.null) + + let confirmParams = + payload + ->Js.Json.decodeObject + ->Belt.Option.flatMap(x => x->Js.Dict.get("confirmParams")) + ->Belt.Option.getWithDefault(Js.Json.null) + + let redirect = + payload + ->Js.Json.decodeObject + ->Belt.Option.flatMap(x => x->Js.Dict.get("redirect")) + ->Belt.Option.flatMap(Js.Json.decodeString) + ->Belt.Option.getWithDefault("if_required") + + let returnUrl = + confirmParams + ->Js.Json.decodeObject + ->Belt.Option.flatMap(x => x->Js.Dict.get("return_url")) + ->Belt.Option.flatMap(Js.Json.decodeString) + ->Belt.Option.getWithDefault("") + + let confirmParam: ConfirmType.confirmParams = { + return_url: returnUrl, + publishableKey, + } + + let paymentIntentID = Js.String2.split(clientSecret, "_secret_")[0]->Option.getOr("") + let endpoint = ApiEndpoint.getApiEndPoint( + ~publishableKey=confirmParam.publishableKey, + ~isConfirmCall=true, + (), + ) + let uri = `${endpoint}/payments/${paymentIntentID}/confirm` + let headers = [ + ("Content-Type", "application/json"), + ("api-key", confirmParam.publishableKey), + ] + + let paymentType: PaymentHelpers.payment = switch paymentMethodType + ->Js.Json.decodeString + ->Belt.Option.getWithDefault("") { + | "apple_pay" => Applepay + | "google_pay" => Gpay + | "debit" + | "credit" + | "" => + Card + | _ => Other + } + + let broswerInfo = BrowserSpec.broswerInfo() + + let body = [ + ("client_secret", clientSecret->Js.Json.string), + ("payment_method", paymentMethod), + ("payment_token", paymentToken), + ("payment_method_type", paymentMethodType), + ] + + let bodyStr = + body->Js.Array2.concat(broswerInfo)->Js.Dict.fromArray->Js.Json.object_->Js.Json.stringify + + PaymentHelpers.intentCall( + ~fetchApi=Utils.fetchApi, + ~uri, + ~headers, + ~bodyStr, + ~confirmParam: ConfirmType.confirmParams, + ~clientSecret, + ~optLogger=Some(logger), + ~handleUserError=false, + ~paymentType, + ~iframeId="", + ~fetchMethod=Fetch.Post, + ~setIsManualRetryEnabled={(. _) => ()}, + ~switchToCustomPod=false, + ~sdkHandleOneClickConfirmPayment=false, + ~counter=0, + ~isPaymentSession=true, + ~paymentSessionRedirect=redirect, + (), + ) + } + + { + getCustomerDefaultSavedPaymentMethodData, + confirmWithCustomerDefaultPaymentMethod, + } + ->customerSavedPaymentMethodsToJson + ->resolve + | None => { + let updatedCustomerDetails = + [ + ( + "error", + [ + ("type", "no_data"->Js.Json.string), + ( + "message", + "There is no customer default saved payment method data"->Js.Json.string, + ), + ] + ->Js.Dict.fromArray + ->Js.Json.object_, + ), + ] + ->Js.Dict.fromArray + ->Js.Json.object_ + updatedCustomerDetails->resolve + } + } + }) + ->catch(err => { + let exceptionMessage = err->Utils.formatException->Js.Json.stringify + let updatedCustomerDetails = + [ + ( + "error", + [("type", "server_error"->Js.Json.string), ("message", exceptionMessage->Js.Json.string)] + ->Js.Dict.fromArray + ->Js.Json.object_, + ), + ] + ->Js.Dict.fromArray + ->Js.Json.object_ + updatedCustomerDetails->resolve + }) +} diff --git a/src/orca-loader/Types.res b/src/orca-loader/Types.res index a37ab20b0..bc19dcb67 100644 --- a/src/orca-loader/Types.res +++ b/src/orca-loader/Types.res @@ -39,6 +39,13 @@ type element = { create: (Js.Dict.key, Js.Json.t) => paymentElement, } +type getCustomerSavedPaymentMethods = { + getCustomerDefaultSavedPaymentMethodData: unit => Js.Json.t, + confirmWithCustomerDefaultPaymentMethod: Js.Json.t => Promise.t, +} + +type initPaymentSession = {getCustomerSavedPaymentMethods: unit => Promise.t} + type confirmParams = {return_url: string} type confirmPaymentParams = { @@ -56,6 +63,7 @@ type hyperInstance = { retrievePaymentIntent: string => Js.Promise.t, widgets: Js.Json.t => element, paymentRequest: Js.Json.t => Js.Json.t, + initPaymentSession: Js.Json.t => initPaymentSession, } let oneClickConfirmPaymentFn = (_, _) => { @@ -115,6 +123,27 @@ let defaultElement = { create, } +let getCustomerDefaultSavedPaymentMethodData = () => { + Js.Json.null +} + +let confirmWithCustomerDefaultPaymentMethod = _confirmParams => { + Js.Promise.resolve(Js.Dict.empty()->Js.Json.object_) +} + +let defaultGetCustomerSavedPaymentMethods = () => { + // TODO: After rescript migration to v11, add this without TAG using enums + // Js.Promise.resolve({ + // getCustomerDefaultSavedPaymentMethodData, + // confirmWithCustomerDefaultPaymentMethod, + // }) + Js.Promise.resolve(Js.Json.null) +} + +let defaultInitPaymentSession: initPaymentSession = { + getCustomerSavedPaymentMethods: defaultGetCustomerSavedPaymentMethods, +} + let defaultHyperInstance = { confirmOneClickPayment: oneClickConfirmPaymentFn, confirmPayment: confirmPaymentFn, @@ -123,6 +152,7 @@ let defaultHyperInstance = { elements: _ev => defaultElement, widgets: _ev => defaultElement, paymentRequest: _ev => Js.Json.null, + initPaymentSession: _ev => defaultInitPaymentSession, } type eventType = diff --git a/src/orca-log-catcher/OrcaLogger.res b/src/orca-log-catcher/OrcaLogger.res index c0c9671df..0a9c2c0f6 100644 --- a/src/orca-log-catcher/OrcaLogger.res +++ b/src/orca-log-catcher/OrcaLogger.res @@ -56,6 +56,7 @@ type eventName = | DISPLAY_VOUCHER | PAYMENT_METHODS_RESPONSE | LOADER_CHANGED + | PAYMENT_SESSION_INITIATED let eventNameToStrMapper = eventName => { switch eventName { @@ -113,6 +114,7 @@ let eventNameToStrMapper = eventName => { | DISPLAY_VOUCHER => "DISPLAY_VOUCHER" | PAYMENT_METHODS_RESPONSE => "PAYMENT_METHODS_RESPONSE" | LOADER_CHANGED => "LOADER_CHANGED" + | PAYMENT_SESSION_INITIATED => "PAYMENT_SESSION_INITIATED" } }