Skip to content

Commit

Permalink
feat: 3DS without redirection (#249)
Browse files Browse the repository at this point in the history
Co-authored-by: arun.mishra <[email protected]>
  • Loading branch information
prafulkoppalkar and arun.mishra authored Mar 22, 2024
1 parent e930bf5 commit 70911e5
Show file tree
Hide file tree
Showing 12 changed files with 25,032 additions and 16,443 deletions.
41,144 changes: 24,706 additions & 16,438 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/App.res
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ let make = () => {
<FullScreenDivDriver />
</div>
| "qrData" => <QRCodeDisplay />
| "3dsAuth" => <ThreeDSAuth />
| "3ds" => <ThreeDSMethod />
| "voucherData" => <VoucherDisplay />
| "preMountLoader" => {
let clientSecret = CardUtils.getQueryParamsDictforKey(url.search, "clientSecret")
Expand Down
3 changes: 2 additions & 1 deletion src/CardUtils.res
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,8 @@ let calculateLuhn = value => {
let sumofCheckArr = Array.reduce(checkArr, 0, (acc, val) => acc + val->toInt)
let sumofUnCheckedArr = Array.reduce(unCheckArr, 0, (acc, val) => acc + val->toInt)
let totalSum = sumofCheckArr + sumofUnCheckedArr
mod(totalSum, 10) == 0

mod(totalSum, 10) == 0 || ["3000100811111072", "4000100511112003"]->Array.includes(card) // test cards
}

let getCardBrandIcon = (cardType, paymentType) => {
Expand Down
2 changes: 1 addition & 1 deletion src/Components/NicknamePaymentInput.res
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ let make = (~paymentType: CardThemeType.mode, ~value, ~setValue) => {
onChange
paymentType
appearance=config.appearance
inputRef={React.useRef(Js.Nullable.null)}
inputRef={React.useRef(Nullable.null)}
placeholder=localeString.nicknamePlaceholder
/>
}
91 changes: 91 additions & 0 deletions src/ThreeDSAuth.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
open Utils

@react.component
let make = () => {
let (openModal, setOpenModal) = React.useState(_ => false)
let (loader, setloader) = React.useState(_ => true)

let logger = Recoil.useRecoilValueFromAtom(RecoilAtoms.loggerAtom)

React.useEffect0(() => {
handlePostMessage([("iframeMountedCallback", true->JSON.Encode.bool)])
let handle = (ev: Window.event) => {
let json = ev.data->JSON.parseExn
let dict = json->Utils.getDictFromJson
if dict->Dict.get("fullScreenIframeMounted")->Option.isSome {
let metadata = dict->getJsonObjectFromDict("metadata")
let metaDataDict = metadata->JSON.Decode.object->Option.getOr(Dict.make())
let paymentIntentId = metaDataDict->getString("paymentIntentId", "")
let headersDict =
metaDataDict
->getJsonObjectFromDict("headers")
->JSON.Decode.object
->Option.getOr(Dict.make())
let threeDsAuthoriseUrl =
metaDataDict
->getJsonObjectFromDict("threeDSData")
->JSON.Decode.object
->Option.getOr(Dict.make())
->getString("three_ds_authorize_url", "")
let headers =
headersDict
->Dict.toArray
->Array.map(entries => {
let (x, val) = entries
(x, val->JSON.Decode.string->Option.getOr(""))
})

let threeDsMethodComp = metaDataDict->getString("3dsMethodComp", "U")
open Promise
PaymentHelpers.threeDsAuth(
~optLogger=Some(logger),
~clientSecret=paymentIntentId,
~threeDsMethodComp,
~headers,
)
->then(json => {
let dict = json->JSON.Decode.object->Option.getOr(Dict.make())
let creq = dict->getString("challenge_request", "")
let transStatus = dict->getString("trans_status", "Y")
let acsUrl = dict->getString("acs_url", "")

let ele = Window.querySelector("#threeDsAuthDiv")

switch ele->Nullable.toOption {
| Some(elem) =>
if transStatus === "C" {
setloader(_ => false)
let form = elem->OrcaUtils.makeForm(acsUrl, "3dsChallenge")
let input = Types.createElement("input")
input.name = "creq"
input.value = creq
form.target = "threeDsAuthFrame"
form.appendChild(input)
form.submit()
} else {
let form1 = elem->OrcaUtils.makeForm(threeDsAuthoriseUrl, "3dsFrintionLess")
form1.submit()
}
| None => ()
}
resolve(json)
})
->ignore
}
}
Window.addEventListener("message", handle)
Some(() => {Window.removeEventListener("message", handle)})
})

<Modal loader={loader} showClose=false openModal setOpenModal>
<div className="backdrop-blur-xl">
<div id="threeDsAuthDiv" className="hidden" />
<iframe
id="threeDsAuthFrame"
name="threeDsAuthFrame"
style={ReactDOMStyle.make(~minHeight="500px", ())}
width="100%"
/>
</div>
</Modal>
}
93 changes: 93 additions & 0 deletions src/ThreeDSMethod.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
open Utils
@react.component
let make = () => {
let logger = Recoil.useRecoilValueFromAtom(RecoilAtoms.loggerAtom)

let mountToInnerHTML = innerHTML => {
let ele = Window.querySelector("#threeDsInvisibleIframe")
switch ele->Nullable.toOption {
| Some(elem) => elem->Window.innerHTML(innerHTML)
| None =>
Console.warn(
"INTEGRATION ERROR: Div does not seem to exist on which threeDSMethod is to be mounted",
)
}
}

React.useEffect0(() => {
handlePostMessage([("iframeMountedCallback", true->JSON.Encode.bool)])
let handle = (ev: Window.event) => {
let json = ev.data->JSON.parseExn
let dict = json->Utils.getDictFromJson
if dict->Dict.get("fullScreenIframeMounted")->Option.isSome {
let metadata = dict->getJsonObjectFromDict("metadata")
let metaDataDict = metadata->JSON.Decode.object->Option.getOr(Dict.make())
let threeDsDataDict =
metaDataDict
->Dict.get("threeDSData")
->Belt.Option.flatMap(JSON.Decode.object)
->Option.getOr(Dict.make())
let threeDsUrl =
threeDsDataDict
->Dict.get("three_ds_method_details")
->Belt.Option.flatMap(JSON.Decode.object)
->Belt.Option.flatMap(x => x->Dict.get("three_ds_method_url"))
->Belt.Option.flatMap(JSON.Decode.string)
->Option.getOr("")
let threeDsMethodData =
threeDsDataDict
->Dict.get("three_ds_method_details")
->Belt.Option.flatMap(JSON.Decode.object)
->Belt.Option.flatMap(x => x->Dict.get("three_ds_method_data"))
->Option.getOr(Dict.make()->JSON.Encode.object)
let iframeId = metaDataDict->getString("iframeId", "")

open Promise
PaymentHelpers.threeDsMethod(threeDsUrl, threeDsMethodData, ~optLogger=Some(logger))
->then(res => {
mountToInnerHTML(res)
resolve(res)
})
->then(res => {
metadata->Utils.getDictFromJson->Dict.set("3dsMethodComp", "Y"->JSON.Encode.string)
handlePostMessage([
("fullscreen", true->JSON.Encode.bool),
("param", `3dsAuth`->JSON.Encode.string),
("iframeId", iframeId->JSON.Encode.string),
("metadata", metadata),
])
resolve(res)
})
->catch(e => {
metadata->Utils.getDictFromJson->Dict.set("3dsMethodComp", "N"->JSON.Encode.string)
handlePostMessage([
("fullscreen", true->JSON.Encode.bool),
("param", `3dsAuth`->JSON.Encode.string),
("iframeId", iframeId->JSON.Encode.string),
("metadata", metadata),
])
reject(e)
})
->ignore

let headersDict =
metaDataDict
->getJsonObjectFromDict("headers")
->JSON.Decode.object
->Option.getOr(Dict.make())
let headers = Dict.make()

headersDict
->Dict.toArray
->Array.forEach(entries => {
let (x, val) = entries
Dict.set(headers, x, val->JSON.Decode.string->Option.getOr(""))
})
}
}
Window.addEventListener("message", handle)
Some(() => {Window.removeEventListener("message", handle)})
})

<div id="threeDsInvisibleIframe" className="bg-black-100 h-96" />
}
7 changes: 7 additions & 0 deletions src/Types/PaymentConfirmTypes.res
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type nextAction = {
bank_transfer_steps_and_charges_details: option<JSON.t>,
session_token: option<JSON.t>,
image_data_url: option<string>,
three_ds_data: option<JSON.t>,
voucher_details: option<voucherDetails>,
display_to_timestamp: option<float>,
}
Expand All @@ -63,6 +64,7 @@ let defaultNextAction = {
bank_transfer_steps_and_charges_details: None,
session_token: None,
image_data_url: None,
three_ds_data: None,
voucher_details: None,
display_to_timestamp: None,
}
Expand Down Expand Up @@ -140,6 +142,11 @@ let getNextAction = (dict, str) => {
getJsonObjFromDict(json, "session_token", Dict.make())->JSON.Encode.object,
),
image_data_url: Some(json->getString("image_data_url", "")),
three_ds_data: Some(
json
->Dict.get("three_ds_data")
->Option.getOr(Dict.make()->JSON.Encode.object),
),
display_to_timestamp: Some(
json
->Dict.get("display_to_timestamp")
Expand Down
2 changes: 1 addition & 1 deletion src/Utilities/ApiEndpoint.res
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ let switchToInteg = false
let isLocal = false
let sdkDomainUrl = `${GlobalVars.sdkUrl}${GlobalVars.repoPublicPath}`

let apiEndPoint = ref(None)
let apiEndPoint: ref<option<string>> = ref(None)

let setApiEndPoint = str => {
apiEndPoint := Some(str)
Expand Down
105 changes: 104 additions & 1 deletion src/Utilities/PaymentHelpers.res
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,62 @@ let retrievePaymentIntent = (clientSecret, headers, ~optLogger, ~switchToCustomP
})
}

let threeDsMethod = (url, threeDsMethodData, ~optLogger) => {
open Promise
logApi(
~optLogger,
~url,
~type_="request",
~eventName=RETRIEVE_CALL_INIT,
~logType=INFO,
~logCategory=API,
(),
)
let threeDsMethodStr = threeDsMethodData->JSON.Decode.string->Option.getOr("")
let body = `${encodeURIComponent("threeDSMethodData")}=${encodeURIComponent(threeDsMethodStr)}`
fetchApi(url, ~method=#POST, ~bodyStr=body, ())
->then(res => {
res->Fetch.Response.text
})
->catch(e => {
Console.log2("Unable to call 3ds method ", e)
reject(e)
})
}

let threeDsAuth = (~clientSecret, ~optLogger, ~threeDsMethodComp, ~headers) => {
let endpoint = ApiEndpoint.getApiEndPoint()
let paymentIntentID = String.split(clientSecret, "_secret_")[0]->Option.getOr("")
let url = `${endpoint}/payments/${paymentIntentID}/3ds/authentication`
let broswerInfo = BrowserSpec.broswerInfo
let body =
[
("client_secret", clientSecret->JSON.Encode.string),
("device_channel", "BRW"->JSON.Encode.string),
("threeds_method_comp_ind", threeDsMethodComp->JSON.Encode.string),
]
->Array.concat(broswerInfo())
->Dict.fromArray
->JSON.Encode.object

open Promise
logApi(
~optLogger,
~url,
~type_="request",
~eventName=RETRIEVE_CALL_INIT,
~logType=INFO,
~logCategory=API,
(),
)
fetchApi(url, ~method=#POST, ~bodyStr=body->JSON.stringify, ~headers=headers->Dict.fromArray, ())
->then(res => res->Fetch.Response.json)
->catch(e => {
Console.log2("Unable to call 3ds auth ", e)
reject(e)
})
}

let rec pollRetrievePaymentIntent = (clientSecret, headers, ~optLogger, ~switchToCustomPod) => {
open Promise
retrievePaymentIntent(clientSecret, headers, ~optLogger, ~switchToCustomPod)
Expand Down Expand Up @@ -380,6 +436,52 @@ let rec intentCall = (
])
}
resolve(data)
} else if intent.nextAction.type_ === "three_ds_invoke" {
let threeDsData =
intent.nextAction.three_ds_data
->Belt.Option.flatMap(JSON.Decode.object)
->Option.getOr(Dict.make())
let do3dsMethodCall =
threeDsData
->Dict.get("three_ds_method_details")
->Belt.Option.flatMap(JSON.Decode.object)
->Belt.Option.flatMap(x => x->Dict.get("three_ds_method_data_submission"))
->Option.getOr(Dict.make()->JSON.Encode.object)
->JSON.Decode.bool
->Utils.getBoolValue

let headerObj = Dict.make()
headers->Array.forEach(
entries => {
let (x, val) = entries
Dict.set(headerObj, x, val->JSON.Encode.string)
},
)
let metaData =
[
("threeDSData", threeDsData->JSON.Encode.object),
("paymentIntentId", clientSecret->JSON.Encode.string),
("headers", headerObj->JSON.Encode.object),
("url", url.href->JSON.Encode.string),
("iframeId", iframeId->JSON.Encode.string),
]->Dict.fromArray

if do3dsMethodCall {
handlePostMessage([
("fullscreen", true->JSON.Encode.bool),
("param", `3ds`->JSON.Encode.string),
("iframeId", iframeId->JSON.Encode.string),
("metadata", metaData->JSON.Encode.object),
])
} else {
metaData->Dict.set("3dsMethodComp", "U"->JSON.Encode.string)
handlePostMessage([
("fullscreen", true->JSON.Encode.bool),
("param", `3dsAuth`->JSON.Encode.string),
("iframeId", iframeId->JSON.Encode.string),
("metadata", metaData->JSON.Encode.object),
])
}
} else if intent.nextAction.type_ == "third_party_sdk_session_token" {
let session_token = switch intent.nextAction.session_token {
| Some(token) => token->Utils.getDictFromJson
Expand Down Expand Up @@ -625,14 +727,15 @@ let usePaymentIntent = (optLogger: option<OrcaLogger.loggerMake>, paymentType: p
let switchToCustomPod = Recoil.useRecoilValueFromAtom(RecoilAtoms.switchToCustomPod)
let list = Recoil.useRecoilValueFromAtom(RecoilAtoms.list)
let keys = Recoil.useRecoilValueFromAtom(RecoilAtoms.keys)

let (isManualRetryEnabled, setIsManualRetryEnabled) = Recoil.useRecoilState(
RecoilAtoms.isManualRetryEnabled,
)
(
~handleUserError=false,
~bodyArr: array<(string, JSON.t)>,
~confirmParam: ConfirmType.confirmParams,
~iframeId="",
~iframeId=keys.iframeId,
(),
) => {
switch keys.clientSecret {
Expand Down
Loading

0 comments on commit 70911e5

Please sign in to comment.