From 05f527abb475bdaf29912fd452a3b0f4b0b3bb55 Mon Sep 17 00:00:00 2001 From: Vrishab Srivatsa <136090360+vsrivatsa-juspay@users.noreply.github.com> Date: Thu, 23 May 2024 16:30:43 +0530 Subject: [PATCH] feat: unsupported card networks validation (#370) Co-authored-by: Pritish Budhiraja <1805317@kiit.ac.in> --- src/CardUtils.res | 1 + src/Components/DynamicFields.res | 2 ++ src/Payment.res | 58 +++++++++++++++++++++++++++++--- src/Payments/CardPayment.res | 26 ++++++++++---- src/RenderPaymentMethods.res | 1 + src/SingleLineCardPayment.res | 1 + src/Utilities/Utils.res | 2 ++ webpack.common.js | 6 ++-- 8 files changed, 83 insertions(+), 14 deletions(-) diff --git a/src/CardUtils.res b/src/CardUtils.res index 26a2d67c1..0fc186571 100644 --- a/src/CardUtils.res +++ b/src/CardUtils.res @@ -17,6 +17,7 @@ type cardIssuer = type cardProps = ( option, (option => option) => unit, + option, string, JsxEvent.Form.t => unit, JsxEvent.Focus.t => unit, diff --git a/src/Components/DynamicFields.res b/src/Components/DynamicFields.res index 638a2097e..42700955c 100644 --- a/src/Components/DynamicFields.res +++ b/src/Components/DynamicFields.res @@ -101,6 +101,7 @@ let make = ( let defaultCardProps = ( None, _ => (), + None, "", _ => (), _ => (), @@ -154,6 +155,7 @@ let make = ( let ( isCardValid, setIsCardValid, + _, cardNumber, changeCardNumber, handleCardBlur, diff --git a/src/Payment.res b/src/Payment.res index a41ae39aa..c4898af15 100644 --- a/src/Payment.res +++ b/src/Payment.res @@ -16,6 +16,7 @@ let make = (~paymentMode, ~integrateError, ~logger) => { let showFields = Recoil.useRecoilValueFromAtom(showCardFieldsAtom) let selectedOption = Recoil.useRecoilValueFromAtom(selectedOptionAtom) let paymentToken = Recoil.useRecoilValueFromAtom(paymentTokenAtom) + let paymentMethodListValue = Recoil.useRecoilValueFromAtom(PaymentUtils.paymentMethodListValue) let {iframeId} = keys @@ -43,6 +44,7 @@ let make = (~paymentMode, ~integrateError, ~logger) => { let (isExpiryValid, setIsExpiryValid) = React.useState(_ => None) let (isCVCValid, setIsCVCValid) = React.useState(_ => None) let (isZipValid, setIsZipValid) = React.useState(_ => None) + let (isCardSupported, setIsCardSupported) = React.useState(_ => None) let (cardBrand, maxCardLength) = React.useMemo(() => { let brand = getCardBrand(cardNumber) @@ -51,6 +53,46 @@ let make = (~paymentMode, ~integrateError, ~logger) => { !showFields && isNotBancontact ? (cardScheme, maxLength) : (brand, maxLength) }, (cardNumber, cardScheme, showFields)) + let supportedCardBrands = React.useMemo(() => { + let cardPaymentMethod = + paymentMethodListValue.payment_methods->Array.find(ele => ele.payment_method === "card") + + switch cardPaymentMethod { + | Some(cardPaymentMethod) => + let cardNetworks = cardPaymentMethod.payment_method_types->Array.map(ele => ele.card_networks) + let cardNetworkNames = + cardNetworks->Array.map(ele => + ele->Array.map( + val => val.card_network->CardUtils.getCardStringFromType->String.toLowerCase, + ) + ) + Some( + cardNetworkNames + ->Array.reduce([], (acc, ele) => acc->Array.concat(ele)) + ->Utils.getUniqueArray, + ) + | None => None + } + }, [paymentMethodListValue]) + + let checkIsCardSupported = cardNumber => { + let cardBrand = cardNumber->CardUtils.getCardBrand + let clearValue = cardNumber->clearSpaces + if cardValid(clearValue, cardBrand) { + switch supportedCardBrands { + | Some(brands) => Some(brands->Array.includes(cardBrand->String.toLowerCase)) + | None => Some(true) + } + } else { + None + } + } + + React.useEffect(() => { + setIsCardSupported(_ => checkIsCardSupported(cardNumber)) + None + }, (supportedCardBrands, cardNumber)) + let cardType = React.useMemo1(() => { cardBrand->getCardType }, [cardBrand]) @@ -92,7 +134,7 @@ let make = (~paymentMode, ~integrateError, ~logger) => { let card = val->formatCardNumber(cardType) let clearValue = card->clearSpaces setCardValid(clearValue, setIsCardValid) - if cardValid(clearValue, cardBrand) { + if cardValid(clearValue, cardBrand) && checkIsCardSupported(clearValue)->Option.getOr(false) { handleInputFocus(~currentRef=cardRef, ~destinationRef=expiryRef) } if card->String.length > 6 && cardNumber->pincodeVisibility { @@ -153,7 +195,7 @@ let make = (~paymentMode, ~integrateError, ~logger) => { let handleCardBlur = ev => { let cardNumber = ReactEvent.Focus.target(ev)["value"] if cardNumberInRange(cardNumber)->Array.includes(true) && calculateLuhn(cardNumber) { - setIsCardValid(_ => Some(true)) + setIsCardValid(_ => checkIsCardSupported(cardNumber)) } else if cardNumber->String.length == 0 { setIsCardValid(_ => None) } else { @@ -336,9 +378,16 @@ let make = (~paymentMode, ~integrateError, ~logger) => { }, (cardNumber, cvcNumber, cardExpiry, isCVCValid, isExpiryValid, isCardValid)) React.useEffect(() => { - setCardError(_ => isCardValid->Option.getOr(true) ? "" : localeString.inValidCardErrorText) + let cardError = if isCardSupported->Option.getOr(true) && isCardValid->Option.getOr(true) { + "" + } else if isCardSupported->Option.getOr(true) { + localeString.inValidCardErrorText + } else { + localeString.cardBrandConfiguredErrorText(cardBrand) + } + setCardError(_ => cardError) None - }, [isCardValid]) + }, [isCardValid, isCardSupported]) React.useEffect(() => { setCvcError(_ => isCVCValid->Option.getOr(true) ? "" : localeString.inCompleteCVCErrorText) @@ -366,6 +415,7 @@ let make = (~paymentMode, ~integrateError, ~logger) => { let cardProps: CardUtils.cardProps = ( isCardValid, setIsCardValid, + isCardSupported, cardNumber, changeCardNumber, handleCardBlur, diff --git a/src/Payments/CardPayment.res b/src/Payments/CardPayment.res index fa1c03192..915df0735 100644 --- a/src/Payments/CardPayment.res +++ b/src/Payments/CardPayment.res @@ -25,6 +25,7 @@ let make = ( let ( isCardValid, setIsCardValid, + isCardSupported, cardNumber, changeCardNumber, handleCardBlur, @@ -99,7 +100,14 @@ let make = ( let areRequiredFieldsValid = Recoil.useRecoilValueFromAtom(RecoilAtoms.areRequiredFieldsValid) - let complete = isAllValid(isCardValid, isCVCValid, isExpiryValid, true, "payment") + let complete = isAllValid( + isCardValid, + isCardSupported, + isCVCValid, + isExpiryValid, + true, + "payment", + ) let empty = cardNumber == "" || cardExpiry == "" || cvcNumber == "" React.useEffect(() => { setComplete(_ => complete) @@ -152,8 +160,13 @@ let make = ( defaultCardBody } if confirm.doSubmit { - let validFormat = (isBancontact || complete) && areRequiredFieldsValid - if validFormat && (showFields || isBancontact) && isCardBrandValid { + let validFormat = + (isBancontact || + (isCVCValid->Option.getOr(false) && + isCardValid->Option.getOr(false) && + isCardSupported->Option.getOr(false) && + isExpiryValid->Option.getOr(false))) && areRequiredFieldsValid + if validFormat && (showFields || isBancontact) { intent( ~bodyArr={ (isBancontact ? banContactBody : cardBody) @@ -179,12 +192,13 @@ let make = ( setCvcError(_ => localeString.cvcNumberEmptyText) setUserError(localeString.enterFieldsText) } + if isCardSupported->Option.getOr(true)->not { + setCardError(_ => localeString.cardBrandConfiguredErrorText(cardBrand)) + setUserError(localeString.cardBrandConfiguredErrorText(cardBrand)) + } if !validFormat { setUserError(localeString.enterValidDetailsText) } - if !isCardBrandValid { - setUserError(localeString.cardBrandConfiguredErrorText(cardBrand)) - } } } }, ( diff --git a/src/RenderPaymentMethods.res b/src/RenderPaymentMethods.res index b6f788e49..f4185a6fd 100644 --- a/src/RenderPaymentMethods.res +++ b/src/RenderPaymentMethods.res @@ -15,6 +15,7 @@ let make = ( let ( isCardValid, setIsCardValid, + _, cardNumber, changeCardNumber, handleCardBlur, diff --git a/src/SingleLineCardPayment.res b/src/SingleLineCardPayment.res index 6e9ba84c5..52287b667 100644 --- a/src/SingleLineCardPayment.res +++ b/src/SingleLineCardPayment.res @@ -16,6 +16,7 @@ let make = ( let ( isCardValid, setIsCardValid, + _, cardNumber, changeCardNumber, handleCardBlur, diff --git a/src/Utilities/Utils.res b/src/Utilities/Utils.res index 27f1f5785..35531437c 100644 --- a/src/Utilities/Utils.res +++ b/src/Utilities/Utils.res @@ -460,12 +460,14 @@ let sortBasedOnPriority = (sortArr: array, priorityArr: array) = let isAllValid = ( card: option, + cardSupported: option, cvc: option, expiry: option, zip: bool, paymentMode: string, ) => { card->getBoolValue && + cardSupported->getBoolValue && cvc->getBoolValue && expiry->getBoolValue && (paymentMode == "payment" || zip) diff --git a/webpack.common.js b/webpack.common.js index e0d800850..ccea631e4 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -6,8 +6,7 @@ const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const CopyPlugin = require("copy-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin"); const TerserPlugin = require("terser-webpack-plugin"); -const BundleAnalyzerPlugin = - require("webpack-bundle-analyzer").BundleAnalyzerPlugin; +const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin; const { sentryWebpackPlugin } = require("@sentry/webpack-plugin"); const sdkEnv = process.env.sdkEnv ?? "local"; @@ -20,8 +19,7 @@ let repoVersion = require("./package.json").version; let majorVersion = "v" + repoVersion.split(".")[0]; let repoName = require("./package.json").name; -let repoPublicPath = - sdkEnv === "local" ? "" : `/${repoVersion}/${majorVersion}`; +let repoPublicPath = sdkEnv === "local" ? "" : `/${repoVersion}/${majorVersion}`; let sdkUrl;