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
39 changes: 22 additions & 17 deletions src/CardElementTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
}

2 changes: 1 addition & 1 deletion src/components/CardNumberElement.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const useCardNumberElement = ({
const [selectedNetwork, setSelectedNetwork] = useState<CardBrand | undefined>(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({
Expand Down
4 changes: 3 additions & 1 deletion src/components/shared/useBrandSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ export const useBrandSelector = ({
const { brand, additional } = binInfo;
const brandOptions = new Set<CardBrand>();

brandOptions.add(convertApiBrandToBrand(brand));
if (brand) {
brandOptions.add(convertApiBrandToBrand(brand));
}

additional?.forEach((a) => {
if (!a.brand) return;
Expand Down
54 changes: 46 additions & 8 deletions src/components/useBinLookup.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<BinInfo | undefined>(undefined);
export const useBinLookup = (enabled: boolean, cardValue: string) => {
const [rawBinInfo, setRawBinInfo] = useState<BinInfo | undefined>(undefined);
const lastBinRef = useRef<string | undefined>(undefined);
const cache = useRef<Map<string, BinInfo | undefined>>(new Map());

const binInfo = useMemo<BinInfo | undefined>(() => {
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;
}
Expand All @@ -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) {
Expand All @@ -62,7 +100,7 @@ export const useBinLookup = (enabled: boolean, bin: string) => {
};

fetchBinInfo();
}, [bin, enabled]);
}, [cardValue, enabled]);

return { binInfo };
};
15 changes: 0 additions & 15 deletions src/utils/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,6 @@ const filterOutMaxOccurrences = (numbers: number[]) =>


const convertApiBrandToBrand = (apiBrandName: string): CardBrand => {
const exceptions: Record<string, CardBrand> = {
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';
};
Expand Down
15 changes: 15 additions & 0 deletions tests/components/CardNumberElement.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,12 @@ describe('CardNumberElement', () => {
country: 'US',
name: 'Test Bank',
},
binRange: [
{
binMin: '424242',
binMax: '424242',
},
],
segment: 'consumer',
additional: [
{
Expand All @@ -421,6 +427,12 @@ describe('CardNumberElement', () => {
country: 'US',
name: 'Test Bank',
},
binRange: [
{
binMin: '424242',
binMax: '424242',
},
],
},
],
};
Expand Down Expand Up @@ -612,6 +624,9 @@ describe('CardNumberElement', () => {
country: 'US',
name: 'Test Bank',
},
binRange: [
{ binMin: '424242', binMax: '424242' },
],
segment: 'consumer',
additional: [],
};
Expand Down