Skip to content

Commit

Permalink
feat: Added Payment Session Headless (#209)
Browse files Browse the repository at this point in the history
Co-authored-by: Praful Koppalkar <[email protected]>
  • Loading branch information
ArushKapoorJuspay and prafulkoppalkar authored Mar 11, 2024
1 parent 180266c commit 67be317
Show file tree
Hide file tree
Showing 10 changed files with 673 additions and 330 deletions.
714 changes: 388 additions & 326 deletions src/Utilities/PaymentHelpers.res

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions src/Utilities/Utils.res
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions src/Window.res
Original file line number Diff line number Diff line change
Expand Up @@ -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"
29 changes: 29 additions & 0 deletions src/orca-loader/Hyper.res
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,34 @@ let make = (publishableKey, options: option<Js.Json.t>, analyticsInfo: option<Js
Window.paymentRequest(methodData, details, optionsForPaymentRequest)
}

let initPaymentSession = paymentSessionOptions => {
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,
Expand All @@ -507,6 +535,7 @@ let make = (publishableKey, options: option<Js.Json.t>, analyticsInfo: option<Js
confirmCardPayment: confirmCardPaymentFn,
retrievePaymentIntent: retrievePaymentIntentFn,
paymentRequest,
initPaymentSession,
}
Window.setHyper(Window.window, returnObject)
returnObject
Expand Down
1 change: 1 addition & 0 deletions src/orca-loader/HyperLoader.res
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ let loadStripe = (str, option) => {

@val external window: {..} = "window"
window["Hyper"] = Hyper.make
window["Hyper"]["init"] = Hyper.make

let isWordpress = window["wp"] !== Js.Json.null
if !isWordpress {
Expand Down
5 changes: 1 addition & 4 deletions src/orca-loader/LoaderPaymentElement.res
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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 => ()
}
Expand Down
26 changes: 26 additions & 0 deletions src/orca-loader/PaymentSession.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
open Types

let make = (options, ~clientSecret, ~publishableKey, ~logger: option<OrcaLogger.loggerMake>) => {
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
}
180 changes: 180 additions & 0 deletions src/orca-loader/PaymentSessionMethods.res
Original file line number Diff line number Diff line change
@@ -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
})
}
30 changes: 30 additions & 0 deletions src/orca-loader/Types.res
Original file line number Diff line number Diff line change
Expand Up @@ -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<Js.Json.t>,
}

type initPaymentSession = {getCustomerSavedPaymentMethods: unit => Promise.t<Js.Json.t>}

type confirmParams = {return_url: string}

type confirmPaymentParams = {
Expand All @@ -56,6 +63,7 @@ type hyperInstance = {
retrievePaymentIntent: string => Js.Promise.t<Js.Json.t>,
widgets: Js.Json.t => element,
paymentRequest: Js.Json.t => Js.Json.t,
initPaymentSession: Js.Json.t => initPaymentSession,
}

let oneClickConfirmPaymentFn = (_, _) => {
Expand Down Expand Up @@ -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,
Expand All @@ -123,6 +152,7 @@ let defaultHyperInstance = {
elements: _ev => defaultElement,
widgets: _ev => defaultElement,
paymentRequest: _ev => Js.Json.null,
initPaymentSession: _ev => defaultInitPaymentSession,
}

type eventType =
Expand Down
2 changes: 2 additions & 0 deletions src/orca-log-catcher/OrcaLogger.res
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ type eventName =
| DISPLAY_VOUCHER
| PAYMENT_METHODS_RESPONSE
| LOADER_CHANGED
| PAYMENT_SESSION_INITIATED

let eventNameToStrMapper = eventName => {
switch eventName {
Expand Down Expand Up @@ -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"
}
}

Expand Down

0 comments on commit 67be317

Please sign in to comment.