Skip to content
Open
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
76 changes: 76 additions & 0 deletions src/Components/PaymentErrorBanner.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
@val @scope("document")
external addDocumentEventListener: (string, _ => unit) => unit = "addEventListener"

@val @scope("document")
external removeDocumentEventListener: (string, _ => unit) => unit = "removeEventListener"

@react.component
let make = () => {
let errorMessage = Recoil.useRecoilValueFromAtom(RecoilAtoms.paymentFailedErrorMessage)
let setErrorMessage = Recoil.useSetRecoilState(RecoilAtoms.paymentFailedErrorMessage)
let {themeObj} = Recoil.useRecoilValueFromAtom(RecoilAtoms.configAtom)

React.useEffect(() => {
if errorMessage->String.length > 0 {
let handler = _event => {
setErrorMessage(_ => "")
}
addDocumentEventListener("input", handler)
addDocumentEventListener("click", handler)
Some(
() => {
removeDocumentEventListener("input", handler)
removeDocumentEventListener("click", handler)
},
)
} else {
None
}
}, [errorMessage])

<RenderIf condition={errorMessage->String.length > 0}>
<div style={paddingTop: "24px"}>
<div
style={
display: "flex",
flexDirection: "row",
alignItems: "flex-start",
gap: themeObj.spacingUnit,
padding: themeObj.spacingTab,
borderRadius: themeObj.borderRadius,
backgroundColor: `${themeObj.colorDanger}15`,
border: `1px solid ${themeObj.colorDanger}40`,
width: "100%",
boxSizing: "border-box",
fontFamily: themeObj.fontFamily,
}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke={themeObj.colorDanger}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
style={
flexShrink: "0",
marginTop: "1px",
}>
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="16" x2="12" y2="12" />
<line x1="12" y1="8" x2="12.01" y2="8" />
</svg>
<span
style={
color: themeObj.colorDangerText,
fontSize: themeObj.fontSizeLg,
lineHeight: "20px",
}>
{React.string(errorMessage)}
</span>
</div>
</div>
</RenderIf>
}
17 changes: 15 additions & 2 deletions src/LoaderController.res
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,24 @@ let make = (~children, ~paymentMode, ~setIntegrateErrorError, ~logger, ~initTime
optionsLocaleString == "" ? config.locale : optionsLocaleString,
)
let constantString = await CardTheme.getConstantStringsObject()
let _ = await S3Utils.initializeCountryData(~locale=config.locale, ~logger)
let rawLocale =
optionsLocaleString != ""
? optionsLocaleString
: config.locale === "auto"
? Window.Navigator.language
: config.locale
// Normalize locale to canonical form using the SDK's locale mapping
// Known locales (e.g., "en-GB", "fr-BE", "zh-Hant") are preserved as-is
// Unknown variants (e.g., "en-US") are mapped to their base language ("en")
let resolvedLocale =
rawLocale
->LocaleStringHelper.mapLocalStringToTypeLocale
->LocaleStringHelper.localeTypeToString
let _ = await S3Utils.initializeCountryData(~locale=resolvedLocale, ~logger)
setConfig(_ => {
config: {
appearance,
locale: config.locale === "auto" ? Window.Navigator.language : config.locale,
locale: resolvedLocale,
fonts: config.fonts,
clientSecret: config.clientSecret,
pmClientSecret: config.pmClientSecret,
Expand Down
25 changes: 25 additions & 0 deletions src/LocaleStrings/LocaleStringHelper.res
Original file line number Diff line number Diff line change
@@ -1,4 +1,29 @@
open LocaleStringTypes

// Converts a locale type to the canonical string used by the backend translations table
let localeTypeToString = locale => {
switch locale {
| EN => "en"
| EN_GB => "en-GB"
| HE => "he"
| FR => "fr"
| FR_BE => "fr-BE"
| AR => "ar"
| JA => "ja"
| DE => "de"
| ES => "es"
| CA => "ca"
| PT => "pt"
| IT => "it"
| PL => "pl"
| NL => "nl"
| SV => "sv"
| RU => "ru"
| ZH => "zh"
| ZH_HANT => "zh-Hant"
}
}

let mapLocalStringToTypeLocale = val => {
// First try the exact match
let exactMatch = switch val->String.toLowerCase {
Expand Down
10 changes: 10 additions & 0 deletions src/PaymentElement.res
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ let make = (~cardProps, ~expiryProps, ~cvcProps, ~paymentType: CardThemeType.mod
customerPaymentMethods,
displaySavedPaymentMethods,
sdkHandleConfirmPayment,
displayPaymentFailureMessage,
} = Recoil.useRecoilValueFromAtom(RecoilAtoms.optionAtom)
let {localeString} = Recoil.useRecoilValueFromAtom(RecoilAtoms.configAtom)
let optionAtomValue = Recoil.useRecoilValueFromAtom(RecoilAtoms.optionAtom)
Expand All @@ -37,6 +38,7 @@ let make = (~cardProps, ~expiryProps, ~cvcProps, ~paymentType: CardThemeType.mod
RecoilAtoms.showPaymentMethodsScreen,
)
let (paymentToken, setPaymentToken) = Recoil.useRecoilState(RecoilAtoms.paymentTokenAtom)
let setPaymentFailedErrorMessage = Recoil.useSetRecoilState(RecoilAtoms.paymentFailedErrorMessage)
let (paymentMethodListValue, setPaymentMethodListValue) = Recoil.useRecoilState(
paymentMethodListValue,
)
Expand Down Expand Up @@ -260,6 +262,11 @@ let make = (~cardProps, ~expiryProps, ~cvcProps, ~paymentType: CardThemeType.mod
None
}, (selectedOption, cardOptions, dropDownOptions, showAllPaymentMethods, layoutClass))

React.useEffect(() => {
setPaymentFailedErrorMessage(_ => "")
None
}, [selectedOption])

let isSelectedOptionValid = React.useMemo(() => {
selectedOption !== "" && paymentOptions->Array.includes(selectedOption)
}, (paymentOptions, selectedOption))
Expand Down Expand Up @@ -606,6 +613,9 @@ let make = (~cardProps, ~expiryProps, ~cvcProps, ~paymentType: CardThemeType.mod
<PaymentElementShimmer />
</RenderIf>
}}
<RenderIf condition={displayPaymentFailureMessage}>
<PaymentErrorBanner />
</RenderIf>
<RenderIf condition={sdkHandleConfirmPayment.handleConfirm}>
<div className="mt-4">
<PayNowButton />
Expand Down
13 changes: 13 additions & 0 deletions src/Types/PaymentConfirmTypes.res
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ type intent = {
payment_method_type: string,
manualRetryAllowed: bool,
connectorTransactionId: string,
userGuidanceMessage: string,
}
open Utils

Expand Down Expand Up @@ -92,6 +93,7 @@ let defaultIntent = {
payment_method_type: "",
manualRetryAllowed: false,
connectorTransactionId: "",
userGuidanceMessage: "",
}

let getAchCreditTransfer = (dict, str) => {
Expand Down Expand Up @@ -186,6 +188,16 @@ let getNextAction = (dict, str) => {
})
->Option.getOr(defaultNextAction)
}
let getUserGuidanceMessage = dict => {
dict
->Dict.get("error_details")
->Option.flatMap(JSON.Decode.object)
->Option.flatMap(errorDetails => errorDetails->Dict.get("unified_details"))
->Option.flatMap(JSON.Decode.object)
->Option.flatMap(unifiedDetails => getOptionString(unifiedDetails, "user_guidance_message"))
->Option.getOr("")
}

let itemToObjMapper = dict => {
{
nextAction: getNextAction(dict, "next_action"),
Expand All @@ -196,5 +208,6 @@ let itemToObjMapper = dict => {
payment_method_type: getString(dict, "payment_method_type", ""),
manualRetryAllowed: getBool(dict, "manual_retry_allowed", false),
connectorTransactionId: getString(dict, "connector_transaction_id", ""),
userGuidanceMessage: getUserGuidanceMessage(dict),
}
}
4 changes: 4 additions & 0 deletions src/Types/PaymentType.res
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ type options = {
customMessageForCardTerms: string,
showShortSurchargeMessage: bool,
paymentMethodsConfig: paymentMethodsConfig,
displayPaymentFailureMessage: bool,
}

type payerDetails = {
Expand Down Expand Up @@ -406,6 +407,7 @@ let defaultOptions = {
customMessageForCardTerms: "",
showShortSurchargeMessage: false,
paymentMethodsConfig: [],
displayPaymentFailureMessage: false,
}

let getMessageDisplayMode = (str, key) => {
Expand Down Expand Up @@ -1239,6 +1241,7 @@ let allowedPaymentElementOptions = [
"customMessageForCardTerms",
"showShortSurchargeMessage",
"paymentMethodsConfig",
"displayPaymentFailureMessage",
]

let fieldsToExcludeFromMasking = ["layout", "wallets", "paymentMethodsConfig", "terms"]
Expand Down Expand Up @@ -1333,6 +1336,7 @@ let itemToObjMapper = (dict, logger: HyperLoggerTypes.loggerMake) => {
customMessageForCardTerms: getString(dict, "customMessageForCardTerms", ""),
showShortSurchargeMessage: getBool(dict, "showShortSurchargeMessage", false),
paymentMethodsConfig: getPaymentMethodsConfig(dict, "paymentMethodsConfig", logger),
displayPaymentFailureMessage: getBool(dict, "displayPaymentFailureMessage", false),
}
}

Expand Down
26 changes: 25 additions & 1 deletion src/Utilities/PaymentHelpers.res
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ let rec intentCall = (
~iframeId,
~fetchMethod,
~setIsManualRetryEnabled,
~setPaymentFailedErrorMessage,
~customPodUri,
~sdkHandleOneClickConfirmPayment,
~counter,
Expand All @@ -344,6 +345,9 @@ let rec intentCall = (
) => {
open Promise
let isConfirm = uri->String.includes("/confirm")
if isConfirm {
setPaymentFailedErrorMessage(_ => "")
}

let isCompleteAuthorize = uri->String.includes("/complete_authorize")
let isPostSessionTokens = uri->String.includes("/post_session_tokens")
Expand Down Expand Up @@ -492,6 +496,7 @@ let rec intentCall = (
~iframeId,
~fetchMethod=#GET,
~setIsManualRetryEnabled,
~setPaymentFailedErrorMessage,
~customPodUri,
~sdkHandleOneClickConfirmPayment,
~counter=counter + 1,
Expand Down Expand Up @@ -894,6 +899,7 @@ let rec intentCall = (
}
if intent.status === "failed" {
setIsManualRetryEnabled(_ => intent.manualRetryAllowed)
setPaymentFailedErrorMessage(_ => intent.userGuidanceMessage)
}
handleProcessingStatus(paymentType, sdkHandleOneClickConfirmPayment)
} else if !isPaymentSession {
Expand Down Expand Up @@ -965,6 +971,7 @@ let rec intentCall = (
~iframeId,
~fetchMethod=#GET,
~setIsManualRetryEnabled,
~setPaymentFailedErrorMessage,
~customPodUri,
~sdkHandleOneClickConfirmPayment,
~counter=counter + 1,
Expand Down Expand Up @@ -1005,11 +1012,13 @@ let usePaymentSync = (optLogger: option<HyperLoggerTypes.loggerMake>, paymentTyp
let customPodUri = Recoil.useRecoilValueFromAtom(customPodUri)
let redirectionFlags = Recoil.useRecoilValueFromAtom(redirectionFlagsAtom)
let setIsManualRetryEnabled = Recoil.useSetRecoilState(isManualRetryEnabled)
let setPaymentFailedErrorMessage = Recoil.useSetRecoilState(paymentFailedErrorMessage)
let {config} = Recoil.useRecoilValueFromAtom(RecoilAtoms.configAtom)
(~handleUserError=false, ~confirmParam: ConfirmType.confirmParams, ~iframeId="") => {
switch keys.clientSecret {
| Some(clientSecret) =>
let paymentIntentID = clientSecret->Utils.getPaymentId
let headers = [("Content-Type", "application/json")]
let headers = [("Content-Type", "application/json"), ("Accept-Language", config.locale)]

switch keys.sdkAuthorization->Utils.getNonEmptyOption {
| Some(_) => ()
Expand Down Expand Up @@ -1039,6 +1048,7 @@ let usePaymentSync = (optLogger: option<HyperLoggerTypes.loggerMake>, paymentTyp
~iframeId,
~fetchMethod=#GET,
~setIsManualRetryEnabled,
~setPaymentFailedErrorMessage,
~customPodUri,
~sdkHandleOneClickConfirmPayment=keys.sdkHandleOneClickConfirmPayment,
~counter=0,
Expand Down Expand Up @@ -1086,9 +1096,11 @@ let useCompleteAuthorizeHandler = () => {

let customPodUri = Recoil.useRecoilValueFromAtom(customPodUri)
let setIsManualRetryEnabled = Recoil.useSetRecoilState(isManualRetryEnabled)
let setPaymentFailedErrorMessage = Recoil.useSetRecoilState(paymentFailedErrorMessage)
let isCallbackUsedVal = Recoil.useRecoilValueFromAtom(isCompleteCallbackUsed)
let redirectionFlags = Recoil.useRecoilValueFromAtom(redirectionFlagsAtom)
let keys = Recoil.useRecoilValueFromAtom(keys)
let {config} = Recoil.useRecoilValueFromAtom(RecoilAtoms.configAtom)

(
~clientSecret: option<string>,
Expand Down Expand Up @@ -1116,6 +1128,8 @@ let useCompleteAuthorizeHandler = () => {
]
}

finalHeaders->Array.push(("Accept-Language", config.locale))

let sdkAuth = switch (
keys.sdkAuthorization->Utils.getNonEmptyOption,
sdkAuthorization->Utils.getNonEmptyOption,
Expand Down Expand Up @@ -1155,6 +1169,7 @@ let useCompleteAuthorizeHandler = () => {
~iframeId,
~fetchMethod=#POST,
~setIsManualRetryEnabled,
~setPaymentFailedErrorMessage,
~customPodUri,
~sdkHandleOneClickConfirmPayment,
~counter=0,
Expand Down Expand Up @@ -1237,6 +1252,8 @@ let usePaymentIntent = (optLogger, paymentType) => {
let redirectionFlags = Recoil.useRecoilValueFromAtom(redirectionFlagsAtom)

let setIsManualRetryEnabled = Recoil.useSetRecoilState(isManualRetryEnabled)
let setPaymentFailedErrorMessage = Recoil.useSetRecoilState(paymentFailedErrorMessage)
let {config} = Recoil.useRecoilValueFromAtom(RecoilAtoms.configAtom)
(
~handleUserError=false,
~bodyArr: array<(string, JSON.t)>,
Expand All @@ -1252,6 +1269,7 @@ let usePaymentIntent = (optLogger, paymentType) => {
let headers = {
let baseHeaders = [
("X-Client-Source", paymentTypeFromUrl->CardThemeType.getPaymentModeToStrMapper),
("Accept-Language", config.locale),
]
switch keys.sdkAuthorization->Utils.getNonEmptyOption {
| Some(sdkAuth) => baseHeaders->Array.push(("Authorization", sdkAuth))
Expand Down Expand Up @@ -1339,6 +1357,7 @@ let usePaymentIntent = (optLogger, paymentType) => {
~iframeId,
~fetchMethod=#POST,
~setIsManualRetryEnabled,
~setPaymentFailedErrorMessage,
~customPodUri,
~sdkHandleOneClickConfirmPayment=keys.sdkHandleOneClickConfirmPayment,
~counter=0,
Expand Down Expand Up @@ -1714,6 +1733,7 @@ let paymentIntentForPaymentSession = (
~iframeId="",
~fetchMethod=#POST,
~setIsManualRetryEnabled={_ => ()},
~setPaymentFailedErrorMessage={_ => ()},
~customPodUri,
~sdkHandleOneClickConfirmPayment=false,
~counter=0,
Expand Down Expand Up @@ -1942,6 +1962,8 @@ let usePostSessionTokens = (
let redirectionFlags = Recoil.useRecoilValueFromAtom(RecoilAtoms.redirectionFlagsAtom)

let setIsManualRetryEnabled = Recoil.useSetRecoilState(isManualRetryEnabled)
let setPaymentFailedErrorMessage = Recoil.useSetRecoilState(paymentFailedErrorMessage)
let {config} = Recoil.useRecoilValueFromAtom(RecoilAtoms.configAtom)
(
~handleUserError=false,
~bodyArr: array<(string, JSON.t)>,
Expand All @@ -1958,6 +1980,7 @@ let usePostSessionTokens = (
let headers = [
("Content-Type", "application/json"),
("X-Client-Source", paymentTypeFromUrl->CardThemeType.getPaymentModeToStrMapper),
("Accept-Language", config.locale),
]

let body = [
Expand Down Expand Up @@ -2036,6 +2059,7 @@ let usePostSessionTokens = (
~iframeId,
~fetchMethod=#POST,
~setIsManualRetryEnabled,
~setPaymentFailedErrorMessage,
~customPodUri,
~sdkHandleOneClickConfirmPayment=keys.sdkHandleOneClickConfirmPayment,
~counter=0,
Expand Down
Loading
Loading