diff --git a/src/CardElementTypes.ts b/src/CardElementTypes.ts index 96a5c12..1aede54 100644 --- a/src/CardElementTypes.ts +++ b/src/CardElementTypes.ts @@ -32,24 +32,29 @@ export const CARD_BRANDS = [ export type CardBrand = (typeof CARD_BRANDS)[number]; export enum CoBadgedSupport { - CartesBancaires = 'cartes-bancaires', + CartesBancaires = 'cartes-bancaires', } +export interface BinRange { + binMin: string; + binMax: string; +} interface CardIssuerDetails { - country: string; - name: string; - } - interface CardInfo { - brand: string; - funding: string; - issuer: CardIssuerDetails; - } - - export interface BinInfo { - brand: string; - funding: string; - issuer: CardIssuerDetails; - segment: string; - additional?: CardInfo[]; - } + country: string; + name: string; +} +interface CardInfo { + brand: string; + funding: string; + issuer: CardIssuerDetails; + binRange?: BinRange[]; +} +export interface BinInfo { + brand?: string; + funding?: string; + issuer?: CardIssuerDetails; + segment?: string; + additional?: CardInfo[]; + binRange?: BinRange[]; +} \ No newline at end of file diff --git a/src/components/CardNumberElement.hook.ts b/src/components/CardNumberElement.hook.ts index 24065f6..56e92aa 100644 --- a/src/components/CardNumberElement.hook.ts +++ b/src/components/CardNumberElement.hook.ts @@ -58,7 +58,7 @@ export const useCardNumberElement = ({ const [selectedNetwork, setSelectedNetwork] = useState(undefined); const binEnabled = binLookup || hasCoBadgedSupport; - const { binInfo } = useBinLookup(binEnabled, elementValue.replaceAll(' ', '').slice(0, 6)); + const { binInfo } = useBinLookup(binEnabled, elementValue.replaceAll(' ', '')); // Get brand options from useBrandSelector hook const { brandSelectorOptions } = useBrandSelector({ diff --git a/src/components/shared/useBrandSelector.ts b/src/components/shared/useBrandSelector.ts index 27b12cd..b241a58 100644 --- a/src/components/shared/useBrandSelector.ts +++ b/src/components/shared/useBrandSelector.ts @@ -32,7 +32,9 @@ export const useBrandSelector = ({ const { brand, additional } = binInfo; const brandOptions = new Set(); - brandOptions.add(convertApiBrandToBrand(brand)); + if (brand) { + brandOptions.add(convertApiBrandToBrand(brand)); + } additional?.forEach((a) => { if (!a.brand) return; diff --git a/src/components/useBinLookup.ts b/src/components/useBinLookup.ts index d99564d..de7cf28 100644 --- a/src/components/useBinLookup.ts +++ b/src/components/useBinLookup.ts @@ -1,8 +1,8 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState, useMemo } from 'react'; import { _useConfigManager, } from '../BasisTheoryProvider'; -import type { BinInfo } from '../CardElementTypes'; +import type { BinInfo, BinRange } from '../CardElementTypes'; export const getBinInfo = async ( bin: string @@ -25,15 +25,53 @@ export const getBinInfo = async ( const data = await response.json(); return (data as BinInfo) || undefined; }; + +const isCardInBinRange = (range: BinRange, cardValue: string) => { + const binLength = Math.min(range.binMin.length, cardValue.length); + const cardBin = Number.parseInt(cardValue?.slice(0, binLength)); + const binMin = Number.parseInt(range.binMin?.slice(0, binLength)); + const binMax = Number.parseInt(range.binMax?.slice(0, binLength)); + return binMin <= cardBin && cardBin <= binMax; +}; -export const useBinLookup = (enabled: boolean, bin: string) => { - const [binInfo, setBinInfo] = useState(undefined); +export const useBinLookup = (enabled: boolean, cardValue: string) => { + const [rawBinInfo, setRawBinInfo] = useState(undefined); const lastBinRef = useRef(undefined); const cache = useRef>(new Map()); + const binInfo = useMemo(() => { + if (!rawBinInfo || !cardValue) { + return undefined; + } + + const primaryRanges = rawBinInfo.binRange || []; + + const isValidPrimaryRange = primaryRanges?.some((range) => + isCardInBinRange(range, cardValue) + ); + + const additionals = rawBinInfo.additional?.filter((additional) => { + const ranges = additional.binRange; + return ranges?.some((range) => isCardInBinRange(range, cardValue)); + }); + + if (!isValidPrimaryRange && !additionals?.length) { + return undefined; + } + + return { + ...(isValidPrimaryRange ? { ...rawBinInfo, binRange: undefined } : {}), + additional: additionals?.map((additional) => ({ + ...additional, + binRange: undefined, + })), + }; + }, [rawBinInfo, cardValue]); + useEffect(() => { + const bin = cardValue?.slice(0, 6); if (!enabled || !bin || bin.length !== 6) { - setBinInfo(undefined); + setRawBinInfo(undefined); lastBinRef.current = undefined; return; } @@ -44,14 +82,14 @@ export const useBinLookup = (enabled: boolean, bin: string) => { const fetchBinInfo = async () => { if (cache.current.has(bin)) { - setBinInfo(cache.current.get(bin)); + setRawBinInfo(cache.current.get(bin)); lastBinRef.current = bin; return; } try { const result = await getBinInfo(bin); - setBinInfo(result); + setRawBinInfo(result); cache.current.set(bin, result); lastBinRef.current = bin; } catch (err: unknown) { @@ -62,7 +100,7 @@ export const useBinLookup = (enabled: boolean, bin: string) => { }; fetchBinInfo(); - }, [bin, enabled]); + }, [cardValue, enabled]); return { binInfo }; }; diff --git a/src/utils/shared.ts b/src/utils/shared.ts index 3f1735b..17ae308 100644 --- a/src/utils/shared.ts +++ b/src/utils/shared.ts @@ -34,21 +34,6 @@ const filterOutMaxOccurrences = (numbers: number[]) => const convertApiBrandToBrand = (apiBrandName: string): CardBrand => { - const exceptions: Record = { - AMEX: 'american-express', - 'DINERS CLUB': 'diners-club', - 'CARTES BANCAIRES': 'cartes-bancaires', - EFTPOS_AUSTRALIA: 'eftpos-australia', - 'KOREAN LOCAL': 'korean-local', - 'PRIVATE LABEL': 'private-label', - }; - - const upperCaseName = apiBrandName.toUpperCase(); - - if (exceptions[upperCaseName]) { - return exceptions[upperCaseName]; - } - const converted = apiBrandName.toLowerCase().replace(/[\s_]+/g, '-') as CardBrand; return converted || 'unknown'; }; diff --git a/tests/components/CardNumberElement.test.tsx b/tests/components/CardNumberElement.test.tsx index a66b998..7c69e45 100644 --- a/tests/components/CardNumberElement.test.tsx +++ b/tests/components/CardNumberElement.test.tsx @@ -412,6 +412,12 @@ describe('CardNumberElement', () => { country: 'US', name: 'Test Bank', }, + binRange: [ + { + binMin: '424242', + binMax: '424242', + }, + ], segment: 'consumer', additional: [ { @@ -421,6 +427,12 @@ describe('CardNumberElement', () => { country: 'US', name: 'Test Bank', }, + binRange: [ + { + binMin: '424242', + binMax: '424242', + }, + ], }, ], }; @@ -612,6 +624,9 @@ describe('CardNumberElement', () => { country: 'US', name: 'Test Bank', }, + binRange: [ + { binMin: '424242', binMax: '424242' }, + ], segment: 'consumer', additional: [], };