Skip to content
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
68 changes: 68 additions & 0 deletions cypress-tests/cypress/e2e/02-cards/01-card-validation.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,74 @@ describe("Card Number Validation", () => {
});
});

describe("CVC Length by Card Brand", () => {
it("should size the CVC to 3 for a Visa", () => {
const { cardNo } = stripeCards.successCard;

getIframeBody()
.find(`[data-testid=${testIds.cardNoInputTestId}]`)
.type(cardNo);

getIframeBody()
.find(`[data-testid=${testIds.cardCVVInputTestId}]`)
.should("have.attr", "maxlength", "3");
});

it("should size the CVC to 4 for an American Express", () => {
const { cardNo } = stripeCards.amexCard15;

getIframeBody()
.find(`[data-testid=${testIds.cardNoInputTestId}]`)
.type(cardNo);

getIframeBody()
.find(`[data-testid=${testIds.cardCVVInputTestId}]`)
.should("have.attr", "maxlength", "4");
});
});

describe("CVC Validity Styling", () => {
// A field is marked invalid on blur and unmarked while focused, uniformly across the
// SDK. The second case matters because the expiry auto-advances into the CVC: focus
// must still land there, and must still clear the mark.
const typeVisaThenShortCvc = () => {
getIframeBody()
.find(`[data-testid=${testIds.cardNoInputTestId}]`)
.type(stripeCards.successCard.cardNo);

getIframeBody()
.find(`[data-testid=${testIds.cardCVVInputTestId}]`)
.type("12");
};

it("should mark an out-of-range CVC invalid on blur and unmark it on focus", () => {
typeVisaThenShortCvc();

getIframeBody().find(`[data-testid=${testIds.cardNoInputTestId}]`).click();
getIframeBody()
.find(`[data-testid=${testIds.cardCVVInputTestId}]`)
.should("have.class", "Input--invalid");

getIframeBody().find(`[data-testid=${testIds.cardCVVInputTestId}]`).click();
getIframeBody()
.find(`[data-testid=${testIds.cardCVVInputTestId}]`)
.should("not.have.class", "Input--invalid");
});

it("should focus the CVC and unmark it when the expiry auto-advances", () => {
typeVisaThenShortCvc();

getIframeBody()
.find(`[data-testid=${testIds.expiryInputTestId}]`)
.type("1230");

getIframeBody()
.find(`[data-testid=${testIds.cardCVVInputTestId}]`)
.should("have.focus")
.and("not.have.class", "Input--invalid");
});
});

describe("Card Brand Icons", () => {
it("should display card brand icon dynamically for Visa", () => {
getIframeBody()
Expand Down
2 changes: 2 additions & 0 deletions src/CardUtils.res
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ type cvcProps = {
onCvcKeyDown: ReactEvent.Keyboard.t => unit,
cvcError: string,
setCvcError: (string => string) => unit,
maxCVCLength: int,
}

let useDefaultCvcProps = () => {
Expand All @@ -110,6 +111,7 @@ let useDefaultCvcProps = () => {
onCvcKeyDown: _ => (),
cvcError: "",
setCvcError: _ => (),
maxCVCLength: CardValidations.getobjFromCardPattern("").maxCVCLength,
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/Components/CardFields.res
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ let make = (
handleCVCBlur,
cvcRef,
cvcError,
maxCVCLength,
} = cvcProps

let isCvcValidValue = CardUtils.getBoolOptionVal(isCVCValid)
Expand Down Expand Up @@ -118,7 +119,7 @@ let make = (
)}
type_="tel"
className={`tracking-widest w-full ${compressedLayoutStyleForCvcError}`}
maxLength=4
maxLength=maxCVCLength
inputRef=cvcRef
placeholder="123"
name=TestUtils.cardCVVInputTestId
Expand Down
41 changes: 33 additions & 8 deletions src/Hooks/CommonCardProps.res
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,22 @@ let useCardForm = (
!showPaymentMethodsScreen && isNotBancontact ? cardScheme : detectedCardBrand
)

let stateCardBrand = cardBrand
let derivedCardBrand = CardUtils.getCardBrandFromStates(
cardBrand,
cardScheme,
showPaymentMethodsScreen,
)
let cardBrand =
cardBrandOverride === "" ? derivedCardBrand : cardBrandOverride->CardUtils.normalizeCardBrand

// Nothing in the card element's tree writes the scheme atom, so the brand is lost here and the CVC
// rules fall back to the permissive default. CVC-scoped: widening cardBrand changes the payload.
let cardBrandForCvc = switch (cardBrand, paymentType) {
| ("", CardThemeType.Card) => stateCardBrand
| _ => cardBrand
}
let maxCVCLength = CardValidations.getobjFromCardPattern(cardBrandForCvc).maxCVCLength
Comment thread
ArushKapoorJuspay marked this conversation as resolved.
let supportedCardBrands = React.useMemo(() => {
switch forwardedSupportedCardBrands {
| Some(brands) => Some(brands)
Expand All @@ -89,12 +98,26 @@ let useCardForm = (
cardBrand->getCardType
}, [cardBrand])

// maxLength only constrains new input, so a CVC entered before the brand was known has to be
// re-judged against it, by the same rule as a blur: narrowing invalidates it, widening restores it.
React.useEffect1(() => {
if (
cvcNumber->String.length > 0 &&
cvcNumberInRange(cvcNumber, cardBrandForCvc)->Array.includes(true)
) {
setIsCVCValid(_ => Some(true))
} else if cvcNumber->String.length == 0 {
setIsCVCValid(_ => None)
} else {
setIsCVCValid(_ => Some(false))
}
None
}, [cardBrandForCvc])

React.useEffect(() => {
let obj = CardValidations.getobjFromCardPattern(cardBrand)
let cvcLength = obj.maxCVCLength
if (
cvcNumberInRange(cvcNumber, cardBrand)->Array.includes(true) &&
cvcNumber->String.length == cvcLength
cvcNumberInRange(cvcNumber, cardBrandForCvc)->Array.includes(true) &&
cvcNumber->String.length == maxCVCLength
) {
blurRef(cvcRef)
}
Expand Down Expand Up @@ -179,13 +202,13 @@ let useCardForm = (
let changeCVCNumber = ev => {
let val = ReactEvent.Form.target(ev)["value"]
logInputChangeInfo("cardCVC", logger)
let cvc = val->CardValidations.formatCVCNumber(cardBrand)
let cvc = val->CardValidations.formatCVCNumber(cardBrandForCvc)
setCvcNumber(_ => cvc)
if cvc->String.length > 0 && cvcNumberInRange(cvc, cardBrand)->Array.includes(true) {
if cvc->String.length > 0 && cvcNumberInRange(cvc, cardBrandForCvc)->Array.includes(true) {
zipRef.current->Nullable.toOption->Option.forEach(input => input->focus)->ignore
}

if cvc->String.length > 0 && cvcNumberInRange(cvc, cardBrand)->Array.includes(true) {
if cvc->String.length > 0 && cvcNumberInRange(cvc, cardBrandForCvc)->Array.includes(true) {
setIsCVCValid(_ => Some(true))
} else {
setIsCVCValid(_ => None)
Expand Down Expand Up @@ -275,7 +298,8 @@ let useCardForm = (
let handleCVCBlur = ev => {
let cvcNumber = ReactEvent.Focus.target(ev)["value"]
if (
cvcNumber->String.length > 0 && cvcNumberInRange(cvcNumber, cardBrand)->Array.includes(true)
cvcNumber->String.length > 0 &&
cvcNumberInRange(cvcNumber, cardBrandForCvc)->Array.includes(true)
) {
setIsCVCValid(_ => Some(true))
} else if cvcNumber->String.length == 0 {
Expand Down Expand Up @@ -390,6 +414,7 @@ let useCardForm = (
onCvcKeyDown,
cvcError,
setCvcError,
maxCVCLength,
}

let zipProps: CardUtils.zipProps = {
Expand Down
3 changes: 2 additions & 1 deletion src/SingleLineCardPayment.res
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ let make = (
handleCVCBlur,
cvcRef,
onCvcKeyDown,
maxCVCLength,
} = cvcProps

let {
Expand Down Expand Up @@ -159,7 +160,7 @@ let make = (
paymentType
type_="tel"
className={`tracking-widest w-auto`}
maxLength=4
maxLength=maxCVCLength
inputRef=cvcRef
placeholder="123"
isFocus
Expand Down
Loading