Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 3DS without redirection #249

Merged
merged 6 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 />
prafulkoppalkar marked this conversation as resolved.
Show resolved Hide resolved
| "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", "")
prafulkoppalkar marked this conversation as resolved.
Show resolved Hide resolved
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")
prafulkoppalkar marked this conversation as resolved.
Show resolved Hide resolved
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)
prafulkoppalkar marked this conversation as resolved.
Show resolved Hide resolved
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)
prafulkoppalkar marked this conversation as resolved.
Show resolved Hide resolved
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
Loading