Skip to content

Commit 204788e

Browse files
committed
feat: hs external vault flow and return user support
1 parent 3939ef6 commit 204788e

15 files changed

Lines changed: 392 additions & 64 deletions

.env

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ SDK_TAG_VERSION=""
77
SENTRY_DSN=""
88
VISA_API_KEY_ID=""
99
VISA_API_CERTIFICATE_PEM=""
10-
PORT= 9050
10+
IS_PCI_COMPLIANT=""
11+
PORT= 9050

src/CardCVCElement.res

Lines changed: 202 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,30 @@
11
open RecoilAtoms
22
open Utils
33

4+
// `isVaultCvcFlow` reuses this CVC element for the Hyperswitch-vault saved-card
5+
// (return user) flow. The single submit handler below has two mutually-exclusive
6+
// paths:
7+
// • default (Elements CVC widget): listens for `requestCVCConfirm` and confirms
8+
// the payment-session intent itself with the entered CVC.
9+
// • vault saved-card flow: listens for the forwarded doSubmit, tokenises the CVC
10+
// through the payment-method-session update call, and posts `savedCardCvcTokenEvent`
11+
// back to SavedMethods, which owns the confirm (mirrors the VGS saved-card flow).
412
@react.component
5-
let make = (~cvcProps: CardUtils.cvcProps, ~paymentType: CardThemeType.mode) => {
13+
let make = (
14+
~cvcProps: CardUtils.cvcProps,
15+
~paymentType: CardThemeType.mode,
16+
~isVaultCvcFlow=false,
17+
) => {
618
let {config, localeString} = Recoil.useRecoilValueFromAtom(configAtom)
719
let emitter = SubscriptionEventHooks.useSubscriptionEventEmitter()
820
let {innerLayout} = config.appearance
921
let keys = Recoil.useRecoilValueFromAtom(keys)
1022
let customPodUri = Recoil.useRecoilValueFromAtom(customPodUri)
1123
let loggerState = Recoil.useRecoilValueFromAtom(loggerAtom)
1224
let redirectionFlags = Recoil.useRecoilValueFromAtom(RecoilAtoms.redirectionFlagsAtom)
25+
// Vault credentials (pmSessionId / sdkAuthorization) for the saved-card tokenise
26+
// call; populated by PaymentMethodsSDK in the inner iframe. Only used in vault mode.
27+
let vaultCredentials = Recoil.useRecoilValueFromAtom(RecoilAtoms.vaultCredentials)
1328

1429
let {
1530
isCVCValid,
@@ -26,9 +41,67 @@ let make = (~cvcProps: CardUtils.cvcProps, ~paymentType: CardThemeType.mode) =>
2641
let compressedLayoutStyleForCvcError =
2742
innerLayout === Compressed && cvcError->String.length > 0 ? "!border-l-0" : ""
2843

29-
React.useEffect(() => {
44+
// Single submit handler for both modes (mutually exclusive on `isVaultCvcFlow`):
45+
// • vault saved-card flow: SavedMethods forwards the validated doSubmit (carrying
46+
// the selected card's payment_token); we tokenise the CVC via the
47+
// payment-method-session update call and post the token back as
48+
// `savedCardCvcTokenEvent`. SavedMethods builds the vault_card_token_data confirm
49+
// body and calls intent. Validation / tokenisation failures post a failed-submit
50+
// response, which SavedMethods forwards to Hyper.res to reject confirmPayment().
51+
// • Elements CVC widget: confirms the payment-session intent itself on the
52+
// `requestCVCConfirm` message, posting the response back to the parent window.
53+
let submitCallback = React.useCallback((ev: Window.event) => {
3054
open Promise
31-
let handleRequestCVCConfirm = (ev: Window.event) => {
55+
if isVaultCvcFlow {
56+
let confirmDict = ev.data->safeParse->getDictFromJson
57+
let confirm = confirmDict->ConfirmType.itemToObjMapper
58+
let isOuterValid = confirmDict->getBool("isOuterValid", true)
59+
60+
if confirm.doSubmit {
61+
let paymentMethodToken = confirmDict->getString("paymentToken", "")
62+
let (pmSessionId, sdkAuthorization) = switch vaultCredentials {
63+
| HyperswitchVault(creds) => (creds.pmSessionId, creds.sdkAuthorization)
64+
| _ => ("", "")
65+
}
66+
let isCvcComplete = cvcNumber->String.length >= 3
67+
68+
if isCvcComplete && isOuterValid {
69+
setCvcError(_ => "")
70+
PaymentHelpersV2.updatePaymentMethod(
71+
~bodyArr=PaymentManagementBody.vaultUpdateCVVBody(~cvcNumber),
72+
~pmSessionId,
73+
~logger=loggerState,
74+
~customPodUri,
75+
~sdkAuthorization,
76+
)
77+
->then(res => {
78+
let vaultTokenData = VaultHelpers.decodeVaultTokenData(res)
79+
if vaultTokenData.token !== "" {
80+
messageParentWindow([
81+
("savedCardCvcTokenEvent", true->JSON.Encode.bool),
82+
("cvcToken", vaultTokenData.token->JSON.Encode.string),
83+
])
84+
} else {
85+
postFailedSubmitResponse(~errortype="server_error", ~message="Something went wrong")
86+
}
87+
resolve()
88+
})
89+
->catch(_ => {
90+
postFailedSubmitResponse(~errortype="server_error", ~message="Something went wrong")
91+
resolve()
92+
})
93+
->ignore
94+
} else if isOuterValid {
95+
// Only the CVC is invalid/empty (outer fields are validated upstream).
96+
let errorMsg =
97+
cvcNumber->String.length == 0
98+
? localeString.cvcNumberEmptyText
99+
: localeString.inCompleteCVCErrorText
100+
setCvcError(_ => errorMsg)
101+
postFailedSubmitResponse(~errortype="validation_error", ~message=errorMsg)
102+
}
103+
}
104+
} else {
32105
let json = ev.data->safeParse
33106
try {
34107
let dict = json->getDictFromJson
@@ -127,13 +200,126 @@ let make = (~cvcProps: CardUtils.cvcProps, ~paymentType: CardThemeType.mode) =>
127200
])
128201
}
129202
}
130-
Window.addEventListener("message", handleRequestCVCConfirm)
131-
Some(
132-
() => {
133-
Window.removeEventListener("message", handleRequestCVCConfirm)
134-
},
135-
)
136-
}, (cvcNumber, keys, paymentType, loggerState, customPodUri, redirectionFlags, localeString))
203+
}, (
204+
isVaultCvcFlow,
205+
cvcNumber,
206+
keys,
207+
paymentType,
208+
loggerState,
209+
customPodUri,
210+
redirectionFlags,
211+
localeString,
212+
vaultCredentials,
213+
))
214+
useSubmitPaymentData(submitCallback)
215+
// React.useEffect(() => {
216+
// open Promise
217+
// let handleRequestCVCConfirm = (ev: Window.event) => {
218+
// let json = ev.data->safeParse
219+
// try {
220+
// let dict = json->getDictFromJson
221+
// switch dict->Dict.get("requestCVCConfirm") {
222+
// | Some(confirmParams) => {
223+
// let confirmParamsDict = confirmParams->getDictFromJson
224+
// let requiresCvv = confirmParamsDict->getBool("requiresCvv", true)
225+
// if paymentType === CardCVCElement {
226+
// let body = confirmParamsDict->getJsonObjectFromDict("body")
227+
// let bodyArr = body->JSON.Decode.object->Option.getOr(Dict.make())->Dict.toArray
228+
// let payload = confirmParamsDict->getJsonFromDict("payload", JSON.Encode.null)
229+
// let paymentTypeStr = confirmParamsDict->getString("paymentType", "card")
230+
// let publishableKeyVal =
231+
// confirmParamsDict->getString("publishableKey", keys.publishableKey)
232+
// let clientSecretVal =
233+
// confirmParamsDict->getString("clientSecret", keys.clientSecret->Option.getOr(""))
234+
235+
// let isCvcComplete = cvcNumber->String.length >= 3
236+
// if requiresCvv && isCvcComplete {
237+
// setCvcError(_ => "")
238+
239+
// let bodyWithCvc =
240+
// bodyArr->Array.concat([("card_cvc", cvcNumber->JSON.Encode.string)])
241+
242+
// let paymentType = paymentTypeStr->PaymentHelpers.getPaymentType
243+
244+
// PaymentHelpers.paymentIntentForPaymentSession(
245+
// ~body=bodyWithCvc,
246+
// ~paymentType,
247+
// ~payload,
248+
// ~publishableKey=publishableKeyVal,
249+
// ~clientSecret=clientSecretVal,
250+
// ~logger=loggerState,
251+
// ~customPodUri,
252+
// ~redirectionFlags,
253+
// ~sdkAuthorization=keys.sdkAuthorization,
254+
// ~mode=CardCVCElement,
255+
// )
256+
// ->then(response => {
257+
// messageParentWindow([("cvcWidgetConfirmResponse", response)])
258+
// resolve()
259+
// })
260+
// ->catch(err => {
261+
// messageParentWindow([
262+
// (
263+
// "cvcWidgetConfirmErrorResponse",
264+
// err->formatException->JSON.stringify->JSON.Encode.string,
265+
// ),
266+
// ])
267+
// resolve()
268+
// })
269+
// ->ignore
270+
// } else if requiresCvv {
271+
// // Future improvement: We can check if the CVC entered is more than 3 digits and show an appropriate error message. For now, we are just checking if it's less than 3 digits.
272+
// let isEmptyCVC = cvcNumber->String.length == 0
273+
274+
// let errorMsg = if isEmptyCVC {
275+
// localeString.cvcNumberEmptyText
276+
// } else {
277+
// localeString.inCompleteCVCErrorText
278+
// }
279+
280+
// setCvcError(_ => errorMsg)
281+
282+
// messageParentWindow([
283+
// (
284+
// "cvcWidgetConfirmErrorResponse",
285+
// handleFailureResponse(~message=errorMsg, ~errorType="cvc_validation_failed"),
286+
// ),
287+
// ])
288+
// } else {
289+
// messageParentWindow([
290+
// (
291+
// "cvcWidgetConfirmErrorResponse",
292+
// handleFailureResponse(
293+
// ~message="Something went wrong",
294+
// ~errorType="cvc_validation_failed",
295+
// ),
296+
// ),
297+
// ])
298+
// }
299+
// }
300+
// }
301+
// | None => ()
302+
// }
303+
// } catch {
304+
// | _ =>
305+
// messageParentWindow([
306+
// (
307+
// "cvcWidgetConfirmErrorResponse",
308+
// handleFailureResponse(
309+
// ~message="Something went wrong",
310+
// ~errorType="cvc_validation_failed",
311+
// ),
312+
// ),
313+
// ])
314+
// }
315+
// }
316+
// Window.addEventListener("message", handleRequestCVCConfirm)
317+
// Some(
318+
// () => {
319+
// Window.removeEventListener("message", handleRequestCVCConfirm)
320+
// },
321+
// )
322+
// }, (cvcNumber, keys, paymentType, loggerState, customPodUri, redirectionFlags, localeString))
137323

138324
React.useEffect(() => {
139325
SubscriptionEventHooks.emitReady(
@@ -144,18 +330,19 @@ let make = (~cvcProps: CardUtils.cvcProps, ~paymentType: CardThemeType.mode) =>
144330
}, (keys.iframeId, paymentType))
145331

146332
React.useEffect(() => {
147-
let cvcInfoDict = [("isCvcEmpty", isCvcEmpty->JSON.Encode.bool)]->Dict.fromArray
148-
Utils.messageParentWindow([("cvcInfo", cvcInfoDict->JSON.Encode.object)])
333+
if !isVaultCvcFlow {
334+
let cvcInfoDict = [("isCvcEmpty", isCvcEmpty->JSON.Encode.bool)]->Dict.fromArray
335+
Utils.messageParentWindow([("cvcInfo", cvcInfoDict->JSON.Encode.object)])
336+
}
149337
None
150338
}, [isCvcEmpty])
151-
152339
React.useEffect(() => {
153340
emitter.emitCvcStatus(~iframeId=keys.iframeId, ~isCvcEmpty, ~isCvcComplete)
154341
None
155342
}, (isCvcEmpty, isCvcComplete, keys.iframeId))
156343

157344
<PaymentInputField
158-
fieldName=localeString.cvcTextLabel
345+
fieldName={isVaultCvcFlow ? "" : localeString.cvcTextLabel}
159346
isValid=isCVCValid
160347
setIsValid=setIsCVCValid
161348
value=cvcNumber
@@ -167,6 +354,7 @@ let make = (~cvcProps: CardUtils.cvcProps, ~paymentType: CardThemeType.mode) =>
167354
maxLength=4
168355
inputRef=cvcRef
169356
placeholder="123"
357+
height={isVaultCvcFlow ? "1.8rem" : ""}
170358
name=TestUtils.cardCVVInputTestId
171359
autocomplete="cc-csc"
172360
/>

src/Components/SavedCardItem.res

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,12 @@ let make = (
6262
~installmentsError,
6363
~setInstallmentsError,
6464
~eligibilitySurchargeDetails: option<EligibilityHelpers.eligibilitySurchargeDetails>,
65-
// VGS saved-card (return user) flow: when true, this card's CVC is collected
66-
// inside the nested iframe (ParentCardComponent saved-card mode) instead of the
67-
// plain input. `setCvcIframeRef` lifts the iframe ref to SavedMethods, which
68-
// owns submit. Defaults keep other call sites (e.g. ClickToPayAuthenticate) intact.
69-
~isVgsCvcFlow=false,
65+
// Vault saved-card (return user) flow (VGS or Hyperswitch): when true, this card's
66+
// CVC is collected inside the nested iframe (ParentCardComponent saved-card mode)
67+
// instead of the plain input. `setCvcIframeRef` lifts the iframe ref to
68+
// SavedMethods, which owns submit. Defaults keep other call sites
69+
// (e.g. ClickToPayAuthenticate) intact.
70+
~isVaultCvcFlow=false,
7071
~setCvcIframeRef=_ => (),
7172
) => {
7273
let {themeObj, config, localeString} = Recoil.useRecoilValueFromAtom(RecoilAtoms.configAtom)
@@ -147,14 +148,14 @@ let make = (
147148
}, (isActive, paymentItem, country, state, pinCode, isCvcEmpty))
148149

149150
React.useEffect(() => {
151+
open CardUtils
150152
if isActive {
151153
// * Focus CVC
152154
focusCVC()
153-
154155
// * Sending card expiry to handle cases where the card expires before the use date.
155156
`${expiryMonth}${String.substring(~start=2, ~end=4, expiryYear)}`
156157
->CardValidations.formatCardExpiryNumber
157-
->CardUtils.emitExpiryDate
158+
->emitExpiryDate
158159
}
159160
None
160161
}, (isActive, paymentItem, country, state, pinCode))
@@ -219,7 +220,7 @@ let make = (
219220
// VGS saved-card flow renders the secure CVC iframe in place of the plain input;
220221
// ParentCardComponent (saved-card mode) hosts it and lifts its ref to SavedMethods.
221222
let makeCvcField = (~fieldName="", ~height="1.8rem", ~inputFieldClassName="flex justify-start") =>
222-
isVgsCvcFlow
223+
isVaultCvcFlow
223224
? <ParentCardComponent
224225
isSavedCardFlow=true containerId=cvcIframeContainerId setExternalIframeRef=setCvcIframeRef
225226
/>

0 commit comments

Comments
 (0)