├── .nvmrc ├── .eslintignore ├── .prettierignore ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request_form.yml │ └── bug_report_form.yml ├── workflows │ ├── ci.yml │ └── security.yml └── pull_request_template.md ├── .eslintrc ├── .gitignore ├── src ├── __tests__ │ ├── .eslintrc │ ├── clone.ts │ ├── helper.ts │ ├── is-valid-input-type.ts │ ├── find-best-match.ts │ ├── add-matching-cards-to-results.ts │ ├── matches.ts │ └── index.ts ├── lib │ ├── is-valid-input-type.ts │ ├── clone.ts │ ├── add-matching-cards-to-results.ts │ ├── find-best-match.ts │ ├── matches.ts │ └── card-types.ts ├── types.ts └── index.ts ├── .npmignore ├── tsconfig.json ├── LICENSE ├── package.json ├── CHANGELOG.md └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | node_modules/* 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *-lock.json 2 | dist 3 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @braintree/team-sdk-js 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "braintree/client" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_STORE 3 | dist/ 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /src/__tests__/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "braintree/es6", 3 | "env": { 4 | "jest": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/__tests__/ 2 | gulpfile.js 3 | .travis.yml 4 | .eslintrc 5 | .eslintignore 6 | .nvmrc 7 | bower.json 8 | .github 9 | -------------------------------------------------------------------------------- /src/lib/is-valid-input-type.ts: -------------------------------------------------------------------------------- 1 | export function isValidInputType(cardNumber: T): boolean { 2 | return typeof cardNumber === "string" || cardNumber instanceof String; 3 | } 4 | -------------------------------------------------------------------------------- /src/lib/clone.ts: -------------------------------------------------------------------------------- 1 | export function clone(originalObject: T): T | null { 2 | if (!originalObject) { 3 | return null; 4 | } 5 | 6 | return JSON.parse(JSON.stringify(originalObject)); 7 | } 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Contact Developer Support 4 | url: https://developer.paypal.com/braintree/help 5 | about: If you need help troubleshooting your integration, reach out to Braintree Support. Only open a GitHub issue if you've found an issue with our SDK. 6 | -------------------------------------------------------------------------------- /src/__tests__/clone.ts: -------------------------------------------------------------------------------- 1 | import { clone } from "../lib/clone"; 2 | 3 | describe("clone", () => { 4 | it("makes a deep copy of an object", () => { 5 | const obj = { foo: "bar" }; 6 | const clonedObj = clone(obj); 7 | 8 | expect(obj).not.toBe(clonedObj); 9 | expect(obj).toEqual(clonedObj); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "esModuleInterop": true, 5 | "outDir": "./dist", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "lib": ["es2015", "dom"], 9 | "strict": true 10 | }, 11 | "include": ["./src/**/*"], 12 | "exclude": ["./src/__tests__/**/*"] 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "Unit Tests" 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | name: "Unit Tests on Ubuntu" 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Use Node.js 13 | uses: actions/setup-node@v1 14 | with: 15 | node-version: "18.x" 16 | - run: npm install 17 | - run: npm test 18 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Summary of changes 2 | 3 | - 4 | 5 | ## Checklist 6 | 7 | - [ ] Added a changelog entry 8 | - [ ] Relevant test coverage 9 | - [ ] Tested and confirmed flows affected by this change are functioning as expected 10 | 11 | ## Authors 12 | > 13 | > List GitHub usernames for everyone who contributed to this pull request. 14 | 15 | ### Reviewers 16 | 17 | @braintree/team-sdk-js 18 | -------------------------------------------------------------------------------- /src/__tests__/helper.ts: -------------------------------------------------------------------------------- 1 | import type { CreditCardType } from "../types"; 2 | 3 | type FakeCreditCardTypeOptions = Partial; 4 | 5 | export function createFakeCreditCardType( 6 | options: FakeCreditCardTypeOptions = {}, 7 | ): CreditCardType { 8 | const defaultOptions = { 9 | niceType: "Nice Type", 10 | type: "type", 11 | patterns: [1], 12 | gaps: [4], 13 | lengths: [16], 14 | code: { 15 | size: 3, 16 | name: "CVV", 17 | }, 18 | }; 19 | 20 | return { 21 | ...defaultOptions, 22 | ...options, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/__tests__/is-valid-input-type.ts: -------------------------------------------------------------------------------- 1 | import { isValidInputType } from "../lib/is-valid-input-type"; 2 | 3 | describe("isValidInputType", () => { 4 | it("returns true if value is a string", () => { 5 | expect(isValidInputType("string")).toBe(true); 6 | }); 7 | 8 | it("returns true if value is a string object", () => { 9 | expect(isValidInputType(String("string"))).toBe(true); 10 | }); 11 | 12 | it("returns false for non-string values", () => { 13 | expect(isValidInputType(12)).toBe(false); 14 | expect(isValidInputType({ foo: "bar" })).toBe(false); 15 | expect(isValidInputType([])).toBe(false); 16 | expect(isValidInputType(false)).toBe(false); 17 | expect(isValidInputType(true)).toBe(false); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /.github/workflows/security.yml: -------------------------------------------------------------------------------- 1 | name: Security 2 | 3 | permissions: 4 | contents: write # Needed by both CodeQL and dependency review 5 | pull-requests: write # Needed by dependency review 6 | statuses: write # Needed by dependency review (to post checks) 7 | security-events: write # Needed by CodeQL to upload SARIF 8 | packages: read # Needed by CodeQL for private/internal packs 9 | actions: read # Needed by CodeQL to access internal actions 10 | 11 | on: 12 | pull_request: 13 | branches: [main] 14 | push: 15 | branches: [main] 16 | workflow_dispatch: 17 | 18 | jobs: 19 | codeql-javascript: 20 | uses: braintree/security-workflows/.github/workflows/codeql.yml@main 21 | with: 22 | language: javascript-typescript 23 | dependency-review: 24 | uses: braintree/security-workflows/.github/workflows/dependency-review.yml@main 25 | -------------------------------------------------------------------------------- /src/lib/add-matching-cards-to-results.ts: -------------------------------------------------------------------------------- 1 | import { clone } from "./clone"; 2 | import { matches } from "./matches"; 3 | import type { CreditCardType } from "../types"; 4 | 5 | export function addMatchingCardsToResults( 6 | cardNumber: string, 7 | cardConfiguration: CreditCardType, 8 | results: Array, 9 | ): void { 10 | let i, patternLength; 11 | 12 | for (i = 0; i < cardConfiguration.patterns.length; i++) { 13 | const pattern = cardConfiguration.patterns[i]; 14 | 15 | if (!matches(cardNumber, pattern)) { 16 | continue; 17 | } 18 | 19 | const clonedCardConfiguration = clone(cardConfiguration) as CreditCardType; 20 | 21 | if (Array.isArray(pattern)) { 22 | patternLength = String(pattern[0]).length; 23 | } else { 24 | patternLength = String(pattern).length; 25 | } 26 | 27 | if (cardNumber.length >= patternLength) { 28 | clonedCardConfiguration.matchStrength = patternLength; 29 | } 30 | 31 | results.push(clonedCardConfiguration); 32 | break; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/__tests__/find-best-match.ts: -------------------------------------------------------------------------------- 1 | import { findBestMatch } from "../lib/find-best-match"; 2 | 3 | import { createFakeCreditCardType } from "./helper"; 4 | 5 | describe("findBestMatch", () => { 6 | it("returns nothing if there are not enough results to try to match", () => { 7 | expect(findBestMatch([])).toBeNull(); 8 | }); 9 | 10 | it("returns nothing if not every element has a matchStrength property", () => { 11 | expect( 12 | findBestMatch([ 13 | createFakeCreditCardType({ matchStrength: 4 }), 14 | createFakeCreditCardType({}), 15 | createFakeCreditCardType({ matchStrength: 5 }), 16 | ]), 17 | ).toBeNull(); 18 | }); 19 | 20 | it("returns the result with the greatest matchStrength value", () => { 21 | const a = createFakeCreditCardType({ matchStrength: 4 }); 22 | const b = createFakeCreditCardType({ matchStrength: 1 }); 23 | const c = createFakeCreditCardType({ matchStrength: 40 }); 24 | const d = createFakeCreditCardType({ matchStrength: 7 }); 25 | const results = [a, b, c, d]; 26 | 27 | expect(findBestMatch(results)).toBe(c); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2018 Braintree, a division of PayPal, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "credit-card-type", 3 | "version": "10.1.0", 4 | "description": "A library for determining credit card type", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "lint": "eslint --ext js,ts .", 9 | "posttest": "npm run lint", 10 | "test": "jest", 11 | "prepublishOnly": "npm run build", 12 | "prebuild": "prettier --write .", 13 | "build": "tsc --declaration" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git@github.com:braintree/credit-card-type" 18 | }, 19 | "homepage": "https://github.com/braintree/credit-card-type", 20 | "author": "", 21 | "license": "MIT", 22 | "devDependencies": { 23 | "@types/jest": "^29.5.3", 24 | "@typescript-eslint/eslint-plugin": "^5.54.1", 25 | "eslint": "^8.47.0", 26 | "eslint-config-braintree": "^6.0.0-typescript-prep-rc.2", 27 | "eslint-plugin-prettier": "^5.0.0", 28 | "jest": "^29.6.3", 29 | "prettier": "^3.0.2", 30 | "ts-jest": "^29.1.1", 31 | "typescript": "^5.1.6" 32 | }, 33 | "jest": { 34 | "preset": "ts-jest", 35 | "testEnvironment": "node", 36 | "testPathIgnorePatterns": [ 37 | "/src/__tests__/helper.ts" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request_form.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Request a new feature. 3 | title: "[FR]: " 4 | labels: ["feature", "request", "enhancement", "triage"] 5 | projects: [] 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thanks for taking the time to fill out this feature request! 11 | - type: textarea 12 | id: feat-summary 13 | attributes: 14 | label: Feature Summary 15 | description: In a few sentences or less, provide a concise description of the feature being requested. 16 | placeholder: "New feature that does ___; it can be used when ..." 17 | validations: 18 | required: true 19 | - type: textarea 20 | id: detailed-description 21 | attributes: 22 | label: Detailed description of new feature. 23 | description: | 24 | Please provide a detailed description of proposed feature. 25 | - What problem would this new feature solve? 26 | - How will this change affect users? 27 | - Is it a modification to existing functionality? Are you requesting a new/modified function input or output? Please provide an example of what the change might look like? 28 | placeholder: "New feature details." 29 | validations: 30 | required: true 31 | -------------------------------------------------------------------------------- /src/lib/find-best-match.ts: -------------------------------------------------------------------------------- 1 | import { CreditCardType } from "../types"; 2 | 3 | function hasEnoughResultsToDetermineBestMatch( 4 | results: CreditCardType[], 5 | ): boolean { 6 | const numberOfResultsWithMaxStrengthProperty = results.filter( 7 | (result) => result.matchStrength, 8 | ).length; 9 | 10 | /* 11 | * if all possible results have a maxStrength property that means the card 12 | * number is sufficiently long enough to determine conclusively what the card 13 | * type is 14 | * */ 15 | return ( 16 | numberOfResultsWithMaxStrengthProperty > 0 && 17 | numberOfResultsWithMaxStrengthProperty === results.length 18 | ); 19 | } 20 | 21 | export function findBestMatch( 22 | results: CreditCardType[], 23 | ): CreditCardType | null { 24 | if (!hasEnoughResultsToDetermineBestMatch(results)) { 25 | return null; 26 | } 27 | 28 | return results.reduce((bestMatch, result) => { 29 | if (!bestMatch) { 30 | return result; 31 | } 32 | 33 | /* 34 | * If the current best match pattern is less specific than this result, set 35 | * the result as the new best match 36 | * */ 37 | if (Number(bestMatch.matchStrength) < Number(result.matchStrength)) { 38 | return result; 39 | } 40 | 41 | return bestMatch; 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /src/lib/matches.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Adapted from https://github.com/polvo-labs/card-type/blob/aaab11f80fa1939bccc8f24905a06ae3cd864356/src/cardType.js#L37-L42 3 | * */ 4 | 5 | function matchesRange( 6 | cardNumber: string, 7 | min: number | string, 8 | max: number | string, 9 | ): boolean { 10 | const maxLengthToCheck = String(min).length; 11 | const substr = cardNumber.substr(0, maxLengthToCheck); 12 | const integerRepresentationOfCardNumber = parseInt(substr, 10); 13 | 14 | min = parseInt(String(min).substr(0, substr.length), 10); 15 | max = parseInt(String(max).substr(0, substr.length), 10); 16 | 17 | return ( 18 | integerRepresentationOfCardNumber >= min && 19 | integerRepresentationOfCardNumber <= max 20 | ); 21 | } 22 | 23 | function matchesPattern(cardNumber: string, pattern: string | number): boolean { 24 | pattern = String(pattern); 25 | 26 | return ( 27 | pattern.substring(0, cardNumber.length) === 28 | cardNumber.substring(0, pattern.length) 29 | ); 30 | } 31 | 32 | export function matches( 33 | cardNumber: string, 34 | pattern: string | number | string[] | number[], 35 | ): boolean { 36 | if (Array.isArray(pattern)) { 37 | return matchesRange(cardNumber, pattern[0], pattern[1]); 38 | } 39 | 40 | return matchesPattern(cardNumber, pattern); 41 | } 42 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type CreditCardTypeCardBrandId = 2 | | "american-express" 3 | | "diners-club" 4 | | "discover" 5 | | "elo" 6 | | "hiper" 7 | | "hipercard" 8 | | "jcb" 9 | | "verve" 10 | | "maestro" 11 | | "mastercard" 12 | | "mir" 13 | | "unionpay" 14 | | "visa"; 15 | 16 | type CreditCardTypeCardBrandNiceType = 17 | | "American Express" 18 | | "Diners Club" 19 | | "Discover" 20 | | "Elo" 21 | | "Hiper" 22 | | "Hipercard" 23 | | "JCB" 24 | | "Maestro" 25 | | "Mastercard" 26 | | "Mir" 27 | | "UnionPay" 28 | | "Visa" 29 | | "Verve"; 30 | 31 | type CreditCardTypeSecurityCodeLabel = 32 | | "CVV" 33 | | "CVC" 34 | | "CID" 35 | | "CVN" 36 | | "CVE" 37 | | "CVP2"; 38 | 39 | export type CreditCardType = { 40 | niceType: string; 41 | type: string; 42 | patterns: (number | number[])[]; 43 | gaps: number[]; 44 | lengths: number[]; 45 | code: { 46 | size: number; 47 | name: string; 48 | }; 49 | matchStrength?: number; 50 | }; 51 | 52 | export interface BuiltInCreditCardType extends CreditCardType { 53 | niceType: CreditCardTypeCardBrandNiceType; 54 | type: CreditCardTypeCardBrandId; 55 | code: { 56 | size: 3 | 4; 57 | name: CreditCardTypeSecurityCodeLabel; 58 | }; 59 | } 60 | 61 | export interface CardCollection { 62 | [propName: string]: CreditCardType; 63 | } 64 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report_form.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report. 3 | title: "[Bug]: " 4 | labels: ["bug", "triage"] 5 | projects: [] 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thanks for taking the time to fill out this bug report! 11 | - type: textarea 12 | id: what-happened 13 | attributes: 14 | label: What happened? 15 | description: Detail the bad behavior and the steps required to duplicate it 16 | placeholder: Tell us what you see! 17 | value: "A bug happened!" 18 | validations: 19 | required: true 20 | - type: textarea 21 | id: expected-behavior 22 | attributes: 23 | label: What did you expect to happen? 24 | description: What did you expect to happen when performing the action? 25 | placeholder: Expected behavior 26 | value: "Expected behavior" 27 | validations: 28 | required: true 29 | - type: input 30 | id: version 31 | attributes: 32 | label: Version 33 | description: What version of this library are you running? 34 | placeholder: "v0.0.0" 35 | value: 36 | validations: 37 | required: true 38 | - type: textarea 39 | id: browsers 40 | attributes: 41 | label: What browsers are you seeing the problem on? 42 | description: | 43 | List browsers where you see the problem reported. For example: 44 | * Mac OS 15.3.5, Chrome 130.0.1234.987 45 | * Android 14, Firefox 133.0 46 | * iPadOS 17.1, Safari 47 | placeholder: "For each browser specify 1) operating system and version 2) browser name and version" 48 | -------------------------------------------------------------------------------- /src/__tests__/add-matching-cards-to-results.ts: -------------------------------------------------------------------------------- 1 | import { addMatchingCardsToResults } from "../lib/add-matching-cards-to-results"; 2 | import { CreditCardType } from "../types"; 3 | import { createFakeCreditCardType } from "./helper"; 4 | 5 | describe("addMatchingCardsToResults", () => { 6 | it("adds a clone of matching card configurations to results array", () => { 7 | const a = createFakeCreditCardType(); 8 | const b = createFakeCreditCardType({ 9 | patterns: [1, 2], 10 | }); 11 | const results = [a]; 12 | 13 | addMatchingCardsToResults("1", b, results); 14 | 15 | expect(results.length).toBe(2); 16 | expect(results[0]).toBe(a); 17 | expect(results[1].patterns).toEqual([1, 2]); 18 | expect(results[1]).not.toBe(b); 19 | }); 20 | 21 | it("does not add a configuration if it does not match", () => { 22 | const a = createFakeCreditCardType(); 23 | const b = createFakeCreditCardType({ 24 | patterns: [1, 2], 25 | }); 26 | const results = [a]; 27 | 28 | addMatchingCardsToResults("3", b, results); 29 | 30 | expect(results.length).toBe(1); 31 | expect(results[0]).toBe(a); 32 | }); 33 | 34 | it("adds a matchStrength property to configuration if card number matches and the length equals or is greater than the pattern length", () => { 35 | const results: CreditCardType[] = []; 36 | 37 | addMatchingCardsToResults( 38 | "304", 39 | createFakeCreditCardType({ patterns: [304] }), 40 | results, 41 | ); 42 | addMatchingCardsToResults( 43 | "304", 44 | createFakeCreditCardType({ patterns: [30] }), 45 | results, 46 | ); 47 | addMatchingCardsToResults( 48 | "304", 49 | createFakeCreditCardType({ patterns: [3045] }), 50 | results, 51 | ); 52 | addMatchingCardsToResults( 53 | "304", 54 | createFakeCreditCardType({ patterns: [3] }), 55 | results, 56 | ); 57 | 58 | expect(results.length).toBe(4); 59 | expect(results[0].matchStrength).toBe(3); 60 | expect(results[1].matchStrength).toBe(2); 61 | expect(results[2].matchStrength).toBeUndefined(); 62 | expect(results[3].matchStrength).toBe(1); 63 | }); 64 | 65 | it("adds a matchStrength property to configuration if card number matches and the length equals or is greater than an entry of the pattern range", () => { 66 | const results: CreditCardType[] = []; 67 | 68 | addMatchingCardsToResults( 69 | "304", 70 | createFakeCreditCardType({ patterns: [[304, 305]] }), 71 | results, 72 | ); 73 | addMatchingCardsToResults( 74 | "304", 75 | createFakeCreditCardType({ patterns: [[30, 99]] }), 76 | results, 77 | ); 78 | addMatchingCardsToResults( 79 | "304", 80 | createFakeCreditCardType({ patterns: [[3045, 4500]] }), 81 | results, 82 | ); 83 | addMatchingCardsToResults( 84 | "304", 85 | createFakeCreditCardType({ patterns: [[3, 5]] }), 86 | results, 87 | ); 88 | 89 | expect(results.length).toBe(4); 90 | expect(results[0].matchStrength).toBe(3); 91 | expect(results[1].matchStrength).toBe(2); 92 | expect(results[2].matchStrength).toBeUndefined(); 93 | expect(results[3].matchStrength).toBe(1); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /src/__tests__/matches.ts: -------------------------------------------------------------------------------- 1 | import { matches } from "../lib/matches"; 2 | 3 | describe("matches", () => { 4 | describe("Array", () => { 5 | it("returns true if value is within range", () => { 6 | const range = ["123", "410"]; 7 | 8 | expect(matches("123", range)).toBe(true); 9 | expect(matches("125", range)).toBe(true); 10 | expect(matches("309", range)).toBe(true); 11 | expect(matches("409", range)).toBe(true); 12 | expect(matches("410", range)).toBe(true); 13 | 14 | expect(matches("122", range)).toBe(false); 15 | expect(matches("010", range)).toBe(false); 16 | expect(matches("411", range)).toBe(false); 17 | expect(matches("999", range)).toBe(false); 18 | }); 19 | 20 | it("returns true if value is within range for partial match", () => { 21 | const range = ["123", "410"]; 22 | 23 | expect(matches("1", range)).toBe(true); 24 | expect(matches("12", range)).toBe(true); 25 | expect(matches("12", range)).toBe(true); 26 | expect(matches("30", range)).toBe(true); 27 | expect(matches("40", range)).toBe(true); 28 | expect(matches("41", range)).toBe(true); 29 | 30 | expect(matches("0", range)).toBe(false); 31 | expect(matches("01", range)).toBe(false); 32 | expect(matches("42", range)).toBe(false); 33 | expect(matches("99", range)).toBe(false); 34 | expect(matches("5", range)).toBe(false); 35 | }); 36 | 37 | it("returns true if value is within range for value with more digits", () => { 38 | const range = ["123", "410"]; 39 | 40 | expect(matches("1230", range)).toBe(true); 41 | expect(matches("1258", range)).toBe(true); 42 | expect(matches("309312123", range)).toBe(true); 43 | expect(matches("409112333", range)).toBe(true); 44 | expect(matches("41056789", range)).toBe(true); 45 | 46 | expect(matches("1220", range)).toBe(false); 47 | expect(matches("0100", range)).toBe(false); 48 | expect(matches("41134567", range)).toBe(false); 49 | expect(matches("99999999", range)).toBe(false); 50 | }); 51 | }); 52 | 53 | describe("Pattern", () => { 54 | it("returns true if value matches the pattern", () => { 55 | const pattern = "123"; 56 | 57 | expect(matches("123", pattern)).toBe(true); 58 | 59 | expect(matches("122", pattern)).toBe(false); 60 | expect(matches("010", pattern)).toBe(false); 61 | expect(matches("411", pattern)).toBe(false); 62 | expect(matches("999", pattern)).toBe(false); 63 | }); 64 | 65 | it("returns true if partial value matches the pattern", () => { 66 | const pattern = "123"; 67 | 68 | expect(matches("", pattern)).toBe(true); 69 | expect(matches("1", pattern)).toBe(true); 70 | expect(matches("12", pattern)).toBe(true); 71 | expect(matches("123", pattern)).toBe(true); 72 | 73 | expect(matches("0", pattern)).toBe(false); 74 | expect(matches("01", pattern)).toBe(false); 75 | expect(matches("124", pattern)).toBe(false); 76 | expect(matches("13", pattern)).toBe(false); 77 | }); 78 | 79 | it("returns true if value matches the pattern when of greater length", () => { 80 | const pattern = "123"; 81 | 82 | expect(matches("1234", pattern)).toBe(true); 83 | expect(matches("1235", pattern)).toBe(true); 84 | expect(matches("1236", pattern)).toBe(true); 85 | expect(matches("1237123", pattern)).toBe(true); 86 | 87 | expect(matches("01234", pattern)).toBe(false); 88 | }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 10.1.0 2 | 3 | - Add support for Verve cards 4 | 5 | # 10.0.2 6 | 7 | - Update (sub-)dependencies 8 | - `cross-spawn` to 7.0.6 9 | - `micromatch` to 4.0.8 10 | - `semver` to 6.3.1 11 | 12 | # 10.0.1 13 | 14 | - Update `braces` to 3.0.3 15 | 16 | # 10.0.0 17 | 18 | - BREAKING CHANGES 19 | - Update Node to v18 20 | - DevDependencies 21 | - Update prettier to v3 22 | - Update eslint-plugin-prettier to v5 23 | - Typescript to v5 24 | 25 | # 9.1.0 26 | 27 | - Add additional Hiper bins (#115 thanks @upigilam) 28 | 29 | # 9.0.1 30 | 31 | - Correct issue where ELO cards were misidentified as Maestro cards (thanks @gabrielozaki) 32 | 33 | # 9.0.0 34 | 35 | - Add typescript types 36 | 37 | _Breaking Changes_ 38 | 39 | - Drop Bower Support 40 | - Drop support for card numbers instantiated with `new String(number)` 41 | 42 | # 8.3.0 43 | 44 | - Add support for series 8 UnionPay cards (fixes #95 thanks @leebradley) 45 | 46 | # 8.2.0 47 | 48 | - Add 14 and 15 length configuration to UnionPay cards 49 | 50 | # 8.1.0 51 | 52 | - Add support for Hiper cards 53 | - Add support for Hipercard cards 54 | 55 | # 8.0.0 56 | 57 | - Improve pattern recognition for card types 58 | 59 | _Breaking Changes_ 60 | 61 | - When adding or updating cards, this module no longer uses an `exactPattern` and `prefixPattern` model. Instead, it takes an array of patterns. See https://github.com/braintree/credit-card-type#pattern-detection for details. 62 | 63 | # 7.1.0 64 | 65 | - Add support for `Elo` card type 66 | - Adds `updateCard` method (#77) 67 | 68 | # 7.0.0 69 | 70 | - Updates "master-card" enum to "mastercard" 71 | 72 | # 6.3.0 73 | 74 | - Add support for `MIR` card type (thanks @antongolub) 75 | 76 | # 6.2.0 77 | 78 | - Allow custom card brands to be added 79 | 80 | # 6.1.1 81 | 82 | - Correct Mastercard bin range for series 2 bins 83 | 84 | # 6.1.0 85 | 86 | - Add support for JCB cards of length 17, 18, and 19 (#54, thanks @zeh) 87 | 88 | # 6.0.0 89 | 90 | - Update mastercard niceType property to `Mastercard` to match new brand guidelines 91 | 92 | **Breaking Changes** 93 | 94 | - Remove internal properties `prefixPattern` and `exactPattern` from returned object 95 | 96 | # 5.0.4 97 | 98 | - Correct Discover to respect lengths for international cards 99 | - Make Maestro pattern more exact 100 | 101 | # 5.0.3 102 | 103 | - Fix prefix pattern for MasterCard numbers starting with `27` 104 | 105 | # 5.0.2 106 | 107 | - Fix checking for UnionPay ranges 108 | 109 | # 5.0.1 110 | 111 | - Visa cards can now be 16, 18, or 19 digits. 112 | 113 | # 5.0.0 114 | 115 | - Card matching has been replaced with a two-tier process. This simplifies the matching process for ambiguous numbers. 116 | 117 | - Partial BIN matches (`prefixPattern`) are accumulated separately from exact BIN matches (`exactPattern`). 118 | - If there were any exact BIN matches, those matches are returned. 119 | - If there are no exact BIN matches, all partial matches are returned. 120 | 121 | # 4.1.0 122 | 123 | - Add `getTypeInfo` and `types` exports for getting type information such as number spacing given a type string such as `visa`. 124 | 125 | # 4.0.3 126 | 127 | - Remove behavior where some UnionPay cards displayed Discover and UnionPay as possible card types 128 | 129 | # 4.0.2 130 | 131 | - Add support for series 2 MasterCard bins (ranging from 222100 to 272099) 132 | - Removes dependency on Lodash 133 | 134 | # 4.0.1 135 | 136 | - Switch to one version of Lodash 137 | 138 | # 4.0.0 139 | 140 | - Further resolve ambiguity issues with various cards; return an array of potential card types instead of a single type 141 | 142 | # 3.0.0 143 | 144 | - Resolve ambiguity issues with Maestro and Discover cards 145 | 146 | # 2.0.0 147 | 148 | - Add support for Maestro and UnionPay 149 | - Change return type of `length` as a `String` to `lengths` as an `Array` 150 | 151 | # 1.0.0 152 | 153 | - Initial Release 154 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import cardTypes = require("./lib/card-types"); 2 | import { addMatchingCardsToResults } from "./lib/add-matching-cards-to-results"; 3 | import { isValidInputType } from "./lib/is-valid-input-type"; 4 | import { findBestMatch } from "./lib/find-best-match"; 5 | import { clone } from "./lib/clone"; 6 | import type { 7 | CreditCardType, 8 | CardCollection, 9 | CreditCardTypeCardBrandId, 10 | } from "./types"; 11 | 12 | let customCards = {} as CardCollection; 13 | 14 | const cardNames: Record = { 15 | VISA: "visa", 16 | MASTERCARD: "mastercard", 17 | AMERICAN_EXPRESS: "american-express", 18 | DINERS_CLUB: "diners-club", 19 | DISCOVER: "discover", 20 | JCB: "jcb", 21 | UNIONPAY: "unionpay", 22 | VERVE: "verve", 23 | MAESTRO: "maestro", 24 | ELO: "elo", 25 | MIR: "mir", 26 | HIPER: "hiper", 27 | HIPERCARD: "hipercard", 28 | }; 29 | 30 | const ORIGINAL_TEST_ORDER = [ 31 | cardNames.VISA, 32 | cardNames.MASTERCARD, 33 | cardNames.AMERICAN_EXPRESS, 34 | cardNames.DINERS_CLUB, 35 | cardNames.DISCOVER, 36 | cardNames.JCB, 37 | cardNames.UNIONPAY, 38 | cardNames.VERVE, 39 | cardNames.MAESTRO, 40 | cardNames.ELO, 41 | cardNames.MIR, 42 | cardNames.HIPER, 43 | cardNames.HIPERCARD, 44 | ]; 45 | 46 | let testOrder = clone(ORIGINAL_TEST_ORDER) as string[]; 47 | 48 | function findType(cardType: string | number): CreditCardType { 49 | return customCards[cardType] || cardTypes[cardType]; 50 | } 51 | 52 | function getAllCardTypes(): CreditCardType[] { 53 | return testOrder.map( 54 | (cardType) => clone(findType(cardType)) as CreditCardType, 55 | ); 56 | } 57 | 58 | function getCardPosition( 59 | name: string, 60 | ignoreErrorForNotExisting = false, 61 | ): number { 62 | const position = testOrder.indexOf(name); 63 | 64 | if (!ignoreErrorForNotExisting && position === -1) { 65 | throw new Error('"' + name + '" is not a supported card type.'); 66 | } 67 | 68 | return position; 69 | } 70 | 71 | function creditCardType(cardNumber: string): Array { 72 | const results = [] as CreditCardType[]; 73 | 74 | if (!isValidInputType(cardNumber)) { 75 | return results; 76 | } 77 | 78 | if (cardNumber.length === 0) { 79 | return getAllCardTypes(); 80 | } 81 | 82 | testOrder.forEach((cardType) => { 83 | const cardConfiguration = findType(cardType); 84 | 85 | addMatchingCardsToResults(cardNumber, cardConfiguration, results); 86 | }); 87 | 88 | const bestMatch = findBestMatch(results) as CreditCardType; 89 | 90 | if (bestMatch) { 91 | return [bestMatch]; 92 | } 93 | 94 | return results; 95 | } 96 | 97 | creditCardType.getTypeInfo = (cardType: string): CreditCardType => 98 | clone(findType(cardType)) as CreditCardType; 99 | 100 | creditCardType.removeCard = (name: string): void => { 101 | const position = getCardPosition(name); 102 | 103 | testOrder.splice(position, 1); 104 | }; 105 | 106 | creditCardType.addCard = (config: CreditCardType): void => { 107 | const existingCardPosition = getCardPosition(config.type, true); 108 | 109 | customCards[config.type] = config; 110 | 111 | if (existingCardPosition === -1) { 112 | testOrder.push(config.type); 113 | } 114 | }; 115 | 116 | creditCardType.updateCard = ( 117 | cardType: string, 118 | updates: Partial, 119 | ): void => { 120 | const originalObject = customCards[cardType] || cardTypes[cardType]; 121 | 122 | if (!originalObject) { 123 | throw new Error( 124 | `"${cardType}" is not a recognized type. Use \`addCard\` instead.'`, 125 | ); 126 | } 127 | 128 | if (updates.type && originalObject.type !== updates.type) { 129 | throw new Error("Cannot overwrite type parameter."); 130 | } 131 | 132 | let clonedCard = clone(originalObject) as CreditCardType; 133 | 134 | clonedCard = { ...clonedCard, ...updates }; 135 | 136 | customCards[clonedCard.type] = clonedCard; 137 | }; 138 | 139 | creditCardType.changeOrder = (name: string, position: number): void => { 140 | const currentPosition = getCardPosition(name); 141 | 142 | testOrder.splice(currentPosition, 1); 143 | testOrder.splice(position, 0, name); 144 | }; 145 | 146 | creditCardType.resetModifications = (): void => { 147 | testOrder = clone(ORIGINAL_TEST_ORDER) as string[]; 148 | customCards = {}; 149 | }; 150 | 151 | creditCardType.types = cardNames; 152 | 153 | export = creditCardType; 154 | -------------------------------------------------------------------------------- /src/lib/card-types.ts: -------------------------------------------------------------------------------- 1 | import type { BuiltInCreditCardType, CardCollection } from "../types"; 2 | 3 | const cardTypes: CardCollection = { 4 | visa: { 5 | niceType: "Visa", 6 | type: "visa", 7 | patterns: [4], 8 | gaps: [4, 8, 12], 9 | lengths: [16, 18, 19], 10 | code: { 11 | name: "CVV", 12 | size: 3, 13 | }, 14 | } as BuiltInCreditCardType, 15 | mastercard: { 16 | niceType: "Mastercard", 17 | type: "mastercard", 18 | patterns: [[51, 55], [2221, 2229], [223, 229], [23, 26], [270, 271], 2720], 19 | gaps: [4, 8, 12], 20 | lengths: [16], 21 | code: { 22 | name: "CVC", 23 | size: 3, 24 | }, 25 | } as BuiltInCreditCardType, 26 | "american-express": { 27 | niceType: "American Express", 28 | type: "american-express", 29 | patterns: [34, 37], 30 | gaps: [4, 10], 31 | lengths: [15], 32 | code: { 33 | name: "CID", 34 | size: 4, 35 | }, 36 | } as BuiltInCreditCardType, 37 | "diners-club": { 38 | niceType: "Diners Club", 39 | type: "diners-club", 40 | patterns: [[300, 305], 36, 38, 39], 41 | gaps: [4, 10], 42 | lengths: [14, 16, 19], 43 | code: { 44 | name: "CVV", 45 | size: 3, 46 | }, 47 | } as BuiltInCreditCardType, 48 | discover: { 49 | niceType: "Discover", 50 | type: "discover", 51 | patterns: [6011, [644, 649], 65], 52 | gaps: [4, 8, 12], 53 | lengths: [16, 19], 54 | code: { 55 | name: "CID", 56 | size: 3, 57 | }, 58 | } as BuiltInCreditCardType, 59 | jcb: { 60 | niceType: "JCB", 61 | type: "jcb", 62 | patterns: [2131, 1800, [3528, 3589]], 63 | gaps: [4, 8, 12], 64 | lengths: [16, 17, 18, 19], 65 | code: { 66 | name: "CVV", 67 | size: 3, 68 | }, 69 | } as BuiltInCreditCardType, 70 | unionpay: { 71 | niceType: "UnionPay", 72 | type: "unionpay", 73 | patterns: [ 74 | 620, 75 | [62100, 62182], 76 | [62184, 62187], 77 | [62185, 62197], 78 | [62200, 62205], 79 | [622010, 622999], 80 | 622018, 81 | [62207, 62209], 82 | [623, 626], 83 | 6270, 84 | 6272, 85 | 6276, 86 | [627700, 627779], 87 | [627781, 627799], 88 | [6282, 6289], 89 | 6291, 90 | 6292, 91 | 810, 92 | [8110, 8131], 93 | [8132, 8151], 94 | [8152, 8163], 95 | [8164, 8171], 96 | ], 97 | gaps: [4, 8, 12], 98 | lengths: [14, 15, 16, 17, 18, 19], 99 | code: { 100 | name: "CVN", 101 | size: 3, 102 | }, 103 | } as BuiltInCreditCardType, 104 | maestro: { 105 | niceType: "Maestro", 106 | type: "maestro", 107 | patterns: [ 108 | 493698, 109 | [500000, 504174], 110 | [504176, 506698], 111 | [506779, 508999], 112 | [56, 59], 113 | 63, 114 | 67, 115 | 6, 116 | ], 117 | gaps: [4, 8, 12], 118 | lengths: [12, 13, 14, 15, 16, 17, 18, 19], 119 | code: { 120 | name: "CVC", 121 | size: 3, 122 | }, 123 | } as BuiltInCreditCardType, 124 | elo: { 125 | niceType: "Elo", 126 | type: "elo", 127 | patterns: [ 128 | 401178, 129 | 401179, 130 | 438935, 131 | 457631, 132 | 457632, 133 | 431274, 134 | 451416, 135 | 457393, 136 | 504175, 137 | [506699, 506778], 138 | [509000, 509999], 139 | 627780, 140 | 636297, 141 | 636368, 142 | [650031, 650033], 143 | [650035, 650051], 144 | [650405, 650439], 145 | [650485, 650538], 146 | [650541, 650598], 147 | [650700, 650718], 148 | [650720, 650727], 149 | [650901, 650978], 150 | [651652, 651679], 151 | [655000, 655019], 152 | [655021, 655058], 153 | ], 154 | gaps: [4, 8, 12], 155 | lengths: [16], 156 | code: { 157 | name: "CVE", 158 | size: 3, 159 | }, 160 | } as BuiltInCreditCardType, 161 | mir: { 162 | niceType: "Mir", 163 | type: "mir", 164 | patterns: [[2200, 2204]], 165 | gaps: [4, 8, 12], 166 | lengths: [16, 17, 18, 19], 167 | code: { 168 | name: "CVP2", 169 | size: 3, 170 | }, 171 | } as BuiltInCreditCardType, 172 | hiper: { 173 | niceType: "Hiper", 174 | type: "hiper", 175 | patterns: [637095, 63737423, 63743358, 637568, 637599, 637609, 637612], 176 | gaps: [4, 8, 12], 177 | lengths: [16], 178 | code: { 179 | name: "CVC", 180 | size: 3, 181 | }, 182 | } as BuiltInCreditCardType, 183 | hipercard: { 184 | niceType: "Hipercard", 185 | type: "hipercard", 186 | patterns: [606282], 187 | gaps: [4, 8, 12], 188 | lengths: [16], 189 | code: { 190 | name: "CVC", 191 | size: 3, 192 | }, 193 | } as BuiltInCreditCardType, 194 | verve: { 195 | niceType: "Verve", 196 | type: "verve", 197 | patterns: [ 198 | [506099, 506127], 199 | 506129, 200 | [506133, 506150], 201 | [506158, 506163], 202 | 506166, 203 | 506168, 204 | 506170, 205 | 506173, 206 | [506176, 506180], 207 | 506184, 208 | [506187, 506188], 209 | 506191, 210 | 506195, 211 | 506197, 212 | 507865, 213 | 507866, 214 | [507868, 507877], 215 | [507880, 507888], 216 | 507900, 217 | 507941, 218 | ], 219 | gaps: [4, 8, 12], 220 | lengths: [16, 18, 19], 221 | code: { 222 | name: "CVV", 223 | size: 3, 224 | }, 225 | } as BuiltInCreditCardType, 226 | }; 227 | 228 | export = cardTypes; 229 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Credit Card Type [![Build Status](https://github.com/braintree/credit-card-type/workflows/Unit%20Tests/badge.svg)](https://github.com/braintree/credit-card-type/actions?query=workflow%3A%22Unit+Tests%22) [![npm version](https://badge.fury.io/js/credit-card-type.svg)](http://badge.fury.io/js/credit-card-type) 2 | 3 | Credit Card Type provides a useful utility method for determining a credit card type from both fully qualified and partial numbers. This is not a validation library but rather a smaller component to help you build your own validation or UI library. 4 | 5 | This library is designed for type-as-you-go detection (supports partial numbers) and is written in CommonJS so you can use it in Node, io.js, and the [browser](http://browserify.org). 6 | 7 | ## Download 8 | 9 | To install via npm: 10 | 11 | ```bash 12 | npm install credit-card-type 13 | ``` 14 | 15 | ## Example 16 | 17 | ```javascript 18 | var creditCardType = require("credit-card-type"); 19 | 20 | // The card number provided should be normalized prior to usage here. 21 | var visaCards = creditCardType("4111"); 22 | console.log(visaCards[0].type); // 'visa' 23 | 24 | var ambiguousCards = creditCardType("6"); 25 | console.log(ambiguousCards.length); // 6 26 | console.log(ambiguousCards[0].niceType); // 'Discover' 27 | console.log(ambiguousCards[1].niceType); // 'UnionPay' 28 | console.log(ambiguousCards[2].niceType); // 'Maestro' 29 | ``` 30 | 31 | ## API 32 | 33 | ### `creditCardType(number: String)` 34 | 35 | `creditCardType` will return an array of objects, each with the following data: 36 | 37 | | Key | Type | Description | 38 | | ---------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 39 | | `niceType` | `String` | A pretty printed representation of the card brand.
- `Visa`
- `Mastercard`
- `American Express`
- `Diners Club`
- `Discover`
- `JCB`
- `UnionPay`
- `Maestro`
- `Mir`
- `Elo`
- `Hiper`
- `Hipercard`
- `Verve` | 40 | | `type` | `String` | A code-friendly presentation of the card brand (useful to class names in CSS). Please refer to Card Type "Constants" below for the list of possible values.
- `visa`
- `mastercard`
- `american-express`
- `diners-club`
- `discover`
- `jcb`
- `unionpay`
- `maestro`
- `mir`
- `elo`
- `hiper`
- `hipercard`
- `verve` | 41 | | `gaps` | `Array` | The expected indices of gaps in a string representation of the card number. For example, in a Visa card, `4111 1111 1111 1111`, there are expected spaces in the 4th, 8th, and 12th positions. This is useful in setting your own formatting rules. | 42 | | `lengths` | `Array` | The expected lengths of the card number as an array of strings (excluding spaces and `/` characters). | 43 | | `code` | `Object` | The information regarding the security code for the determined card. Learn more about the [code object](#code) below. | 44 | 45 | If no card types are found, this returns an empty array. 46 | 47 | _Note:_ The card number provided should be normalized ahead of time. The card number string should not contain any non-integer values (e.g. no letters or special characters) 48 | 49 | ### `creditCardType.getTypeInfo(type: String)` 50 | 51 | `getTypeInfo` will return a singular object (with the same structure as `creditCardType`) corresponding with the specified `type`, or undefined if the specified `type` is invalid/unknown. 52 | 53 | ### Card Type "Constants" 54 | 55 | Named variables are provided for each of the supported card types: 56 | 57 | - `AMERICAN_EXPRESS` 58 | - `DINERS_CLUB` 59 | - `DISCOVER` 60 | - `ELO` 61 | - `HIPERCARD` 62 | - `HIPER` 63 | - `JCB` 64 | - `MAESTRO` 65 | - `MASTERCARD` 66 | - `MIR` 67 | - `UNIONPAY` 68 | - `VISA` 69 | - `VERVE` 70 | 71 | #### `code` 72 | 73 | Card brands provide different nomenclature for their security codes as well as varying lengths. 74 | 75 | | Brand | Name | Size | 76 | | ------------------ | ------ | ---- | 77 | | `Visa` | `CVV` | 3 | 78 | | `Mastercard` | `CVC` | 3 | 79 | | `American Express` | `CID` | 4 | 80 | | `Diners Club` | `CVV` | 3 | 81 | | `Discover` | `CID` | 3 | 82 | | `JCB` | `CVV` | 3 | 83 | | `UnionPay` | `CVN` | 3 | 84 | | `Maestro` | `CVC` | 3 | 85 | | `Mir` | `CVP2` | 3 | 86 | | `Elo` | `CVE` | 3 | 87 | | `Hiper` | `CVC` | 3 | 88 | | `Hipercard` | `CVC` | 4 | 89 | | `Verve` | `CVV` | 3 | 90 | 91 | A full response for a `Visa` card will look like this: 92 | 93 | ```json 94 | { 95 | "niceType": "Visa", 96 | "type": "visa", 97 | "gaps": [4, 8, 12], 98 | "lengths": [16], 99 | "code": { "name": "CVV", "size": 3 } 100 | } 101 | ``` 102 | 103 | ### Advanced Usage 104 | 105 | CommonJS: 106 | 107 | ```javascript 108 | var creditCardType = require("credit-card-type"); 109 | var getTypeInfo = require("credit-card-type").getTypeInfo; 110 | var CardType = require("credit-card-type").types; 111 | ``` 112 | 113 | ES6: 114 | 115 | ```javascript 116 | import creditCardType, { 117 | getTypeInfo, 118 | types as CardType, 119 | } from "credit-card-type"; 120 | ``` 121 | 122 | #### Filtering 123 | 124 | ```javascript 125 | creditCardType(cardNumber).filter(function (card) { 126 | return card.type === CardType.MASTERCARD || card.type === CardType.VISA; 127 | }); 128 | ``` 129 | 130 | #### Pattern Detection 131 | 132 | Each card type has a `patterns` attribute that is an array of numbers and ranges of numbers (represented by an array of 2 values, a min and a max). 133 | 134 | If the pattern is a number, the modules compares it against the card number. Partial matches for card numbers that are shorter than the pattern also match. Given the pattern `123`, then the card numbers `1`, `12`, `123`, `1234` will all match, but `2`, `13`, and `124` will not. 135 | 136 | If the pattern is an array of numbers, then the card number is checked to be within the range of those numbers. Again, partial matches are accepted. Given the range `[100, 123]`, then the card numbers `1`, `10`, `100`, `12`, `120`, 137 | `123` will all match, but `2`, `13`, and `124` will not. 138 | 139 | For detection, the module loops over each card type's `patterns` array, and if a match occurs, that card type is added to the array of results. 140 | 141 | In the case where multiple matches are made, if the entirety of the pattern is matched, the card type with the stronger pattern is preferred. For instance, Visa cards match anything that starts with a 4, but there are 142 | some Elo cards that begin with a 4. One example is `401178`. So for the card 143 | numbers, `4`, `40`, `401`, `4011`, `40117`, the module will report that this 144 | card is _either_ a Visa or an Elo card. Once the card number becomes `401178`, 145 | the modules sees that an exact match for the ELO bin has been made, and the module reports 146 | that the card can only be an Elo card. 147 | 148 | #### Adding Card Types 149 | 150 | You can add additional card brands not supported by the module with `addCard`. Pass in the configuration object. 151 | 152 | ```javascript 153 | creditCardType.addCard({ 154 | niceType: "NewCard", 155 | type: "new-card", 156 | patterns: [2345, 2376], 157 | gaps: [4, 8, 12], 158 | lengths: [16], 159 | code: { 160 | name: "CVV", 161 | size: 3, 162 | }, 163 | }); 164 | ``` 165 | 166 | If you add a card that already exists in the module, it will overwrite it. 167 | 168 | ```javascript 169 | creditCardType.addCard({ 170 | niceType: "Visa with Custom Nice Type", 171 | type: creditCardType.types.VISA, 172 | patterns: [41111, [44, 47]], 173 | gaps: [4, 8, 12], 174 | lengths: [13, 16, 19], // add support for old, deprecated 13 digit visas 175 | code: { 176 | name: "CVV", 177 | size: 3, 178 | }, 179 | }); 180 | ``` 181 | 182 | Adding new cards puts them at the bottom of the priority for testing. Priority is determined by an array. By default, the priority looks like: 183 | 184 | ```javascript 185 | [ 186 | creditCardType.types.VISA, 187 | creditCardType.types.MASTERCARD, 188 | creditCardType.types.AMERICAN_EXPRESS, 189 | creditCardType.types.DINERS_CLUB, 190 | creditCardType.types.DISCOVER, 191 | creditCardType.types.JCB, 192 | creditCardType.types.UNIONPAY, 193 | creditCardType.types.VERVE 194 | creditCardType.types.MAESTRO, 195 | creditCardType.types.ELO, 196 | creditCardType.types.MIR, 197 | creditCardType.types.HIPER, 198 | creditCardType.types.HIPERCARD, 199 | ]; 200 | ``` 201 | 202 | You can adjust the order using `changeOrder`. The number you pass in as the second argument is where the card is inserted into the array. The closer to the beginning of the array, the higher priority it has. 203 | 204 | ```javascript 205 | creditCardType.changeOrder("my-new-card", 0); // give custom card type the highest priority 206 | creditCardType.changeOrder("my-new-card", 3); // give it a priority at position 3 in the test order array 207 | ``` 208 | 209 | You can also remove cards with `removeCard`. 210 | 211 | ```javscript 212 | creditCardType.removeCard(creditCardType.types.VISA); 213 | ``` 214 | 215 | If you need to reset the modifications you have created, simply call `resetModifications`: 216 | 217 | ```javascript 218 | creditCardType.resetModifications(); 219 | ``` 220 | 221 | #### Updating Card Types 222 | 223 | You can update cards with `updateCard`. Pass in the card type and the configuration object. Any properties left off will inherit from the original card object. 224 | 225 | ```javascript 226 | creditCardType.updateCard(creditCardType.types.VISA, { 227 | niceType: "Fancy Visa", 228 | lengths: [11, 16], 229 | }); 230 | 231 | var visa = creditCardType.getTypeInfo(creditCardType.types.VISA); 232 | 233 | // overwritten properties 234 | visa.niceType; // 'Fancy Visa' 235 | visa.length; // [11, 16] 236 | 237 | // unchanged properties 238 | visa.gaps; // [4, 8, 12] 239 | visa.code.name; // 'CVV' 240 | ``` 241 | 242 | If you need to reset the modifications you have created, simply call `resetModifications`: 243 | 244 | ```javascript 245 | creditCardType.resetModifications(); 246 | ``` 247 | 248 | #### Pretty Card Numbers 249 | 250 | ```javascript 251 | function prettyCardNumber(cardNumber, cardType) { 252 | var card = getTypeInfo(cardType); 253 | 254 | if (card) { 255 | var offsets = [].concat(0, card.gaps, cardNumber.length); 256 | var components = []; 257 | 258 | for (var i = 0; offsets[i] < cardNumber.length; i++) { 259 | var start = offsets[i]; 260 | var end = Math.min(offsets[i + 1], cardNumber.length); 261 | components.push(cardNumber.substring(start, end)); 262 | } 263 | 264 | return components.join(" "); 265 | } 266 | 267 | return cardNumber; 268 | } 269 | 270 | prettyCardNumber("xxxxxxxxxx343", CardType.AMERICAN_EXPRESS); // 'xxxx xxxxxx 343' 271 | ``` 272 | 273 | ### Development 274 | 275 | We use `nvm` for managing our node versions, but you do not have to. Replace any `nvm` references with the tool of your choice below. 276 | 277 | ```bash 278 | nvm install 279 | npm install 280 | ``` 281 | 282 | All testing dependencies will be installed upon `npm install` and the test suite executed with `npm test`. 283 | -------------------------------------------------------------------------------- /src/__tests__/index.ts: -------------------------------------------------------------------------------- 1 | import creditCardType = require("../index"); 2 | 3 | describe("creditCardType", () => { 4 | it.each([ 5 | ["411", "visa"], 6 | ["4111111111111111", "visa"], 7 | ["4012888888881881", "visa"], 8 | ["4222222222222", "visa"], 9 | ["4462030000000000", "visa"], 10 | ["4484070000000000", "visa"], 11 | ["411111111111111111", "visa"], 12 | ["4111111111111111110", "visa"], 13 | 14 | ["431274", "elo"], 15 | ["451416", "elo"], 16 | ["457393", "elo"], 17 | ["401178", "elo"], 18 | ["401179", "elo"], 19 | ["438935", "elo"], 20 | ["457631", "elo"], 21 | ["457632", "elo"], 22 | ["4576321111111111", "elo"], 23 | ["5066991111111118", "elo"], 24 | ["504175", "elo"], 25 | ["6277809", "elo"], 26 | ["6277809990229178", "elo"], 27 | ["650033", "elo"], 28 | ["6500331111111111", "elo"], 29 | 30 | ["2221", "mastercard"], 31 | ["2222", "mastercard"], 32 | ["2223", "mastercard"], 33 | ["2224", "mastercard"], 34 | ["2225", "mastercard"], 35 | ["2226", "mastercard"], 36 | ["2225", "mastercard"], 37 | ["2226", "mastercard"], 38 | ["223", "mastercard"], 39 | ["2239", "mastercard"], 40 | ["23", "mastercard"], 41 | ["24", "mastercard"], 42 | ["25", "mastercard"], 43 | ["26", "mastercard"], 44 | ["27", "mastercard"], 45 | ["270", "mastercard"], 46 | ["271", "mastercard"], 47 | ["272", "mastercard"], 48 | ["2720", "mastercard"], 49 | 50 | ["506099", "verve"], 51 | ["506100", "verve"], 52 | ["506127", "verve"], 53 | ["506129", "verve"], 54 | ["506133", "verve"], 55 | ["506150", "verve"], 56 | ["506158", "verve"], 57 | ["506163", "verve"], 58 | ["506166", "verve"], 59 | ["506170", "verve"], 60 | ["506176", "verve"], 61 | ["506180", "verve"], 62 | ["506195", "verve"], 63 | ["507865", "verve"], 64 | ["507888", "verve"], 65 | ["507941", "verve"], 66 | ["5061001234567890123", "verve"], 67 | 68 | ["51", "mastercard"], 69 | ["52", "mastercard"], 70 | ["53", "mastercard"], 71 | ["54", "mastercard"], 72 | ["55", "mastercard"], 73 | ["5555555555554444", "mastercard"], 74 | ["5454545454545454", "mastercard"], 75 | 76 | ["34", "american-express"], 77 | ["37", "american-express"], 78 | ["341", "american-express"], 79 | ["34343434343434", "american-express"], 80 | ["378282246310005", "american-express"], 81 | ["371449635398431", "american-express"], 82 | ["378734493671000", "american-express"], 83 | 84 | ["30", "diners-club"], 85 | ["300", "diners-club"], 86 | ["36", "diners-club"], 87 | ["38", "diners-club"], 88 | ["39", "diners-club"], 89 | ["30569309025904", "diners-club"], 90 | ["38520000023237", "diners-club"], 91 | ["36700102000000", "diners-club"], 92 | ["36148900647913", "diners-club"], 93 | 94 | ["6011", "discover"], 95 | ["644", "discover"], 96 | ["644", "discover"], 97 | ["645", "discover"], 98 | ["646", "discover"], 99 | ["647", "discover"], 100 | ["648", "discover"], 101 | ["649", "discover"], 102 | ["6011000400000000", "discover"], 103 | ["6011111111111117", "discover"], 104 | ["6011000990139424", "discover"], 105 | 106 | ["62123456789002", "unionpay"], 107 | ["621234567890003", "unionpay"], 108 | ["6221258812340000", "unionpay"], 109 | ["622018111111111111", "unionpay"], 110 | ["6212345678900000003", "unionpay"], 111 | 112 | ["56", "maestro"], 113 | ["57", "maestro"], 114 | ["58", "maestro"], 115 | ["59", "maestro"], 116 | ["67", "maestro"], 117 | ["6304000000000000", "maestro"], 118 | ["6799990100000000019", "maestro"], 119 | ["62183", "maestro"], 120 | 121 | ["1", "jcb"], 122 | ["35", "jcb"], 123 | ["2131", "jcb"], 124 | ["21312", "jcb"], 125 | ["1800", "jcb"], 126 | ["18002", "jcb"], 127 | ["3530111333300000", "jcb"], 128 | ["3566002020360505", "jcb"], 129 | ["35308796121637357", "jcb"], 130 | ["353445444300732639", "jcb"], 131 | ["3537286818376838569", "jcb"], 132 | 133 | ["6221260000000000", "unionpay"], 134 | ["6221260000000000000", "unionpay"], 135 | ["6222000000000000", "unionpay"], 136 | ["6228000000000000", "unionpay"], 137 | ["6229250000000000", "unionpay"], 138 | ["6229250000000000000", "unionpay"], 139 | ["6240000000000000", "unionpay"], 140 | ["6260000000000000000", "unionpay"], 141 | ["6282000000000000", "unionpay"], 142 | ["6289000000000000000", "unionpay"], 143 | ["6221558812340000", "unionpay"], 144 | ["6269992058134322", "unionpay"], 145 | ["622018111111111111", "unionpay"], 146 | ["8", "unionpay"], 147 | ["8100513433325374", "unionpay"], 148 | ["8111700872004845", "unionpay"], 149 | ["8141618644273338", "unionpay"], 150 | ["8158163233706018", "unionpay"], 151 | ["8168524506870054", "unionpay"], 152 | 153 | ["220", "mir"], 154 | ["2200", "mir"], 155 | ["2204", "mir"], 156 | ["22000000000000000", "mir"], 157 | ["22049999999999999", "mir"], 158 | 159 | ["6062820524845321", "hipercard"], 160 | ["6062820000", "hipercard"], 161 | ["6370950000000005", "hiper"], 162 | ["637095", "hiper"], 163 | ["637609", "hiper"], 164 | ["637599", "hiper"], 165 | ["637612", "hiper"], 166 | ["637568", "hiper"], 167 | ["63737423", "hiper"], 168 | ["63743358", "hiper"], 169 | ])("Matches %s to brand %s", (number, cardType) => { 170 | const actual = creditCardType(number); 171 | 172 | try { 173 | expect(actual).toHaveLength(1); 174 | } catch (e) { 175 | console.log(actual); // eslint-disable-line no-console 176 | throw e; 177 | } 178 | expect(actual[0].type).toBe(cardType); 179 | }); 180 | 181 | it.each([ 182 | [ 183 | "", 184 | [ 185 | "visa", 186 | "mastercard", 187 | "american-express", 188 | "diners-club", 189 | "discover", 190 | "jcb", 191 | "unionpay", 192 | "verve", 193 | "maestro", 194 | "elo", 195 | "mir", 196 | "hiper", 197 | "hipercard", 198 | ], 199 | ], 200 | ["2", ["mastercard", "jcb", "mir"]], 201 | ["3", ["american-express", "diners-club", "jcb"]], 202 | ["5", ["mastercard", "verve", "maestro", "elo"]], 203 | ["50", ["verve", "maestro", "elo"]], 204 | ["6", ["discover", "unionpay", "maestro", "elo", "hiper", "hipercard"]], 205 | ["60", ["discover", "maestro", "hipercard"]], 206 | ["601", ["discover", "maestro"]], 207 | ["64", ["discover", "maestro"]], 208 | ["62", ["unionpay", "maestro", "elo"]], 209 | 210 | ["4", ["visa", "maestro", "elo"]], 211 | ["43", ["visa", "elo"]], 212 | ["431", ["visa", "elo"]], 213 | ["4312", ["visa", "elo"]], 214 | ["43127", ["visa", "elo"]], 215 | ["45141", ["visa", "elo"]], 216 | ["45739", ["visa", "elo"]], 217 | ["40117", ["visa", "elo"]], 218 | ["43893", ["visa", "elo"]], 219 | ["45763", ["visa", "elo"]], 220 | 221 | ["506099", ["verve"]], 222 | ["506100", ["verve"]], 223 | ["506195", ["verve"]], 224 | ["507865", ["verve"]], 225 | ["507888", ["verve"]], 226 | ["50794", ["verve", "maestro"]], 227 | ["507941", ["verve"]], 228 | 229 | ["6277", ["unionpay", "maestro", "elo"]], 230 | ["62778", ["unionpay", "maestro", "elo"]], 231 | 232 | ["63", ["maestro", "elo", "hiper"]], 233 | ["636", ["maestro", "elo"]], 234 | ["6362", ["maestro", "elo"]], 235 | ["63629", ["maestro", "elo"]], 236 | 237 | ["637", ["maestro", "hiper"]], 238 | ["637374", ["maestro", "hiper"]], 239 | ["637433", ["maestro", "hiper"]], 240 | 241 | ["606", ["maestro", "hipercard"]], 242 | 243 | ["627", ["unionpay", "maestro", "elo"]], 244 | ["6062", ["maestro", "hipercard"]], 245 | ["6370", ["maestro", "hiper"]], 246 | ["6376", ["maestro", "hiper"]], 247 | ["6375", ["maestro", "hiper"]], 248 | ["65", ["discover", "maestro", "elo"]], 249 | ["655", ["discover", "maestro", "elo"]], 250 | ["6550", ["discover", "maestro", "elo"]], 251 | ["65502", ["discover", "maestro", "elo"]], 252 | ])("Matches %s to array %p", (number, expectedNames) => { 253 | const actualNames = creditCardType(number).map((cardType) => cardType.type); 254 | 255 | expect(expectedNames).toEqual(actualNames); 256 | }); 257 | 258 | it.each([ 259 | "0", 260 | "12", 261 | "123", 262 | "181", 263 | "1802", 264 | "221", 265 | "222099", 266 | "2721", 267 | "212", 268 | "2132", 269 | "306", 270 | "31", 271 | "32", 272 | "33", 273 | "7", 274 | "9", 275 | ])("returns an empty array for %s", (unknown) => { 276 | expect(creditCardType(unknown)).toHaveLength(0); 277 | }); 278 | 279 | it.each([ 280 | ["Mastercard", "5454545454545454", { size: 3, name: "CVC" }], 281 | ["Visa", "4111111111111111", { size: 3, name: "CVV" }], 282 | ["American Express", "378734493671000", { size: 4, name: "CID" }], 283 | ["Discover", "6011000990139424", { size: 3, name: "CID" }], 284 | ["JCB", "30569309025904", { size: 3, name: "CVV" }], 285 | ["Diners Club", "30569309025904", { size: 3, name: "CVV" }], 286 | ["UnionPay", "6220558812340000", { size: 3, name: "CVN" }], 287 | ["Maestro", "6304000000000000", { size: 3, name: "CVC" }], 288 | ["Mir", "2200000000000000", { size: 3, name: "CVP2" }], 289 | ["Verve", "5061001234567890", { size: 3, name: "CVV" }], 290 | ])("returns security codes for %s", (brand, number, code) => { 291 | const parsedCode = creditCardType(number)[0].code; 292 | 293 | expect(parsedCode).toMatchObject(code); 294 | }); 295 | 296 | it.each([ 297 | [ 298 | "Maestro", 299 | "6304000000000000", 300 | { type: "maestro", lengths: [12, 13, 14, 15, 16, 17, 18, 19] }, 301 | ], 302 | ["Diners Club", "305", { type: "diners-club", lengths: [14, 16, 19] }], 303 | ["Discover", "6011", { type: "discover", lengths: [16, 19] }], 304 | ["Visa", "4", { type: "visa", lengths: [16, 18, 19] }], 305 | ["Mastercard", "54", { type: "mastercard", lengths: [16] }], 306 | ["JCB", "35", { type: "jcb", lengths: [16, 17, 18, 19] }], 307 | ["Mir", "220", { type: "mir", lengths: [16, 17, 18, 19] }], 308 | ["Verve", "5061", { type: "verve", lengths: [16, 18, 19] }], 309 | ])("returns lengths for %s", (brand, number, meta) => { 310 | const cardType = creditCardType(number)[0]; 311 | 312 | expect(cardType).toMatchObject(meta); 313 | }); 314 | 315 | it("preserves integrity of returned values", () => { 316 | const result = creditCardType("4111111111111111")[0]; 317 | 318 | result.type = "whaaaaaat"; 319 | expect(creditCardType("4111111111111111")[0].type).toBe("visa"); 320 | }); 321 | }); 322 | 323 | describe("getTypeInfo", () => { 324 | it("returns card type information", () => { 325 | const info = creditCardType.getTypeInfo(creditCardType.types.VISA); 326 | 327 | expect(info.type).toBe("visa"); 328 | expect(info.niceType).toBe("Visa"); 329 | }); 330 | 331 | it("returns null for an unknown card type", () => { 332 | expect(creditCardType.getTypeInfo("gibberish")).toBeNull(); 333 | }); 334 | }); 335 | 336 | describe("resetModifications", () => { 337 | it("resets card removals", () => { 338 | const original = creditCardType(""); 339 | 340 | creditCardType.removeCard("mastercard"); 341 | 342 | expect(creditCardType("")).not.toEqual(original); 343 | 344 | creditCardType.resetModifications(); 345 | 346 | expect(creditCardType("")).toEqual(original); 347 | }); 348 | 349 | it("resets card additions", () => { 350 | const original = creditCardType(""); 351 | 352 | creditCardType.addCard({ 353 | niceType: "NewCard", 354 | type: "new-card", 355 | patterns: [2345], 356 | gaps: [4, 8, 12], 357 | lengths: [16], 358 | code: { 359 | name: "cvv", 360 | size: 3, 361 | }, 362 | }); 363 | 364 | expect(creditCardType("")).not.toEqual(original); 365 | 366 | creditCardType.resetModifications(); 367 | 368 | expect(creditCardType("")).toEqual(original); 369 | }); 370 | 371 | it("resets card modifications", () => { 372 | const original = creditCardType(""); 373 | 374 | creditCardType.addCard({ 375 | niceType: "Custom Visa Nice Type", 376 | type: "visa", 377 | patterns: [4], 378 | gaps: [4, 8, 12], 379 | lengths: [16], 380 | code: { 381 | name: "Security Code", 382 | size: 3, 383 | }, 384 | }); 385 | 386 | expect(creditCardType("")).not.toEqual(original); 387 | 388 | creditCardType.resetModifications(); 389 | 390 | expect(creditCardType("")).toEqual(original); 391 | }); 392 | 393 | it("resets order changes", () => { 394 | const original = creditCardType(""); 395 | 396 | creditCardType.changeOrder("visa", 4); 397 | 398 | expect(creditCardType("")).not.toEqual(original); 399 | 400 | creditCardType.resetModifications(); 401 | 402 | expect(creditCardType("")).toEqual(original); 403 | }); 404 | }); 405 | 406 | describe("removeCard", () => { 407 | afterEach(() => { 408 | creditCardType.resetModifications(); 409 | }); 410 | 411 | it("removes card from test order array", () => { 412 | creditCardType.removeCard("mastercard"); 413 | 414 | const result = creditCardType("2"); 415 | 416 | expect(result).toHaveLength(2); 417 | expect(result[0].type).toBe("jcb"); 418 | expect(result[1].type).toBe("mir"); 419 | }); 420 | 421 | it("throws an error if card type is passed which is not in the array", () => { 422 | expect(() => { 423 | creditCardType.removeCard("bogus"); 424 | }).toThrowError('"bogus" is not a supported card type.'); 425 | }); 426 | }); 427 | 428 | describe("addCard", () => { 429 | afterEach(() => { 430 | creditCardType.resetModifications(); 431 | }); 432 | 433 | it("adds new nested card types correctly", () => { 434 | // this test is mostly prove nested CreditCardType.patterns are valid 435 | // see https://github.com/braintree/credit-card-type/pull/147 for inciting issue 436 | creditCardType.addCard({ 437 | niceType: "NestedCard", 438 | type: "nested-card", 439 | patterns: [41111, [44, 47]], 440 | gaps: [4, 8, 12], 441 | lengths: [16], 442 | code: { 443 | name: "cvv", 444 | size: 3, 445 | }, 446 | }); 447 | 448 | const result = creditCardType("4"); 449 | 450 | expect(result).toHaveLength(4); 451 | expect(result[0].type).toBe("visa"); 452 | expect(result[1].type).toBe("maestro"); 453 | expect(result[2].type).toBe("elo"); 454 | expect(result[3].type).toBe("nested-card"); 455 | }); 456 | 457 | it("adds new card type", () => { 458 | creditCardType.addCard({ 459 | niceType: "NewCard", 460 | type: "new-card", 461 | patterns: [2345], 462 | gaps: [4, 8, 12], 463 | lengths: [16], 464 | code: { 465 | name: "cvv", 466 | size: 3, 467 | }, 468 | }); 469 | 470 | const result = creditCardType("2"); 471 | 472 | expect(result).toHaveLength(4); 473 | expect(result[0].type).toBe("mastercard"); 474 | expect(result[1].type).toBe("jcb"); 475 | expect(result[2].type).toBe("mir"); 476 | expect(result[3].type).toBe("new-card"); 477 | }); 478 | 479 | it("can overwrite existing cards", () => { 480 | let result = creditCardType("4111111"); 481 | 482 | expect(result).toHaveLength(1); 483 | expect(result[0].type).toBe("visa"); 484 | expect(result[0].niceType).toBe("Visa"); 485 | expect(result[0].code.name).toBe("CVV"); 486 | expect(result[0].lengths).toEqual([16, 18, 19]); 487 | 488 | creditCardType.addCard({ 489 | niceType: "Custom Visa Nice Type", 490 | type: "visa", 491 | patterns: [4], 492 | gaps: [4, 8, 12], 493 | lengths: [16], 494 | code: { 495 | name: "Security Code", 496 | size: 3, 497 | }, 498 | }); 499 | 500 | result = creditCardType("4111111"); 501 | 502 | expect(result).toHaveLength(1); 503 | expect(result[0].type).toBe("visa"); 504 | expect(result[0].niceType).toBe("Custom Visa Nice Type"); 505 | expect(result[0].code.name).toBe("Security Code"); 506 | expect(result[0].lengths).toEqual([16]); 507 | }); 508 | 509 | it("adds new card to last position in card list", () => { 510 | let result = creditCardType("2"); 511 | 512 | expect(result).toHaveLength(3); 513 | 514 | creditCardType.addCard({ 515 | niceType: "NewCard", 516 | type: "new-card", 517 | patterns: [2345], 518 | gaps: [4, 8, 12], 519 | lengths: [16], 520 | code: { 521 | name: "CVV", 522 | size: 3, 523 | }, 524 | }); 525 | 526 | result = creditCardType("2"); 527 | 528 | expect(result).toHaveLength(4); 529 | expect(result[3].type).toBe("new-card"); 530 | 531 | creditCardType.addCard({ 532 | niceType: "NewCard 2", 533 | type: "another-new-card", 534 | patterns: [2345], 535 | gaps: [4, 8, 12], 536 | lengths: [16], 537 | code: { 538 | name: "CVV", 539 | size: 3, 540 | }, 541 | }); 542 | 543 | result = creditCardType("2"); 544 | 545 | expect(result).toHaveLength(5); 546 | expect(result[3].type).toBe("new-card"); 547 | expect(result[4].type).toBe("another-new-card"); 548 | }); 549 | }); 550 | 551 | describe("updateCard", () => { 552 | afterEach(() => { 553 | creditCardType.resetModifications(); 554 | }); 555 | 556 | it("throws an error if the card type does not exist", () => { 557 | expect(() => { 558 | creditCardType.updateCard("foo", {}); 559 | }).toThrowError('"foo" is not a recognized type. Use `addCard` instead.'); 560 | }); 561 | 562 | it("throws an error if the type field in the updates object exists and does not match", () => { 563 | expect(() => { 564 | creditCardType.updateCard(creditCardType.types.VISA, { 565 | type: "not visa", 566 | }); 567 | }).toThrowError("Cannot overwrite type parameter."); 568 | }); 569 | 570 | it("does not throw an error if the type field in the updates object exists and does match", () => { 571 | expect(() => { 572 | creditCardType.updateCard(creditCardType.types.VISA, { 573 | type: "visa", 574 | }); 575 | }).not.toThrowError(); 576 | }); 577 | 578 | it("updates existing card", () => { 579 | creditCardType.updateCard(creditCardType.types.VISA, { 580 | niceType: "Fancy Visa", 581 | lengths: [11, 16, 18, 19], 582 | }); 583 | 584 | const updatedVisa = creditCardType.getTypeInfo(creditCardType.types.VISA); 585 | 586 | expect(updatedVisa.niceType).toBe("Fancy Visa"); 587 | expect(updatedVisa.lengths).toEqual([11, 16, 18, 19]); 588 | expect(updatedVisa.gaps).toEqual([4, 8, 12]); 589 | expect(updatedVisa.code).toEqual({ 590 | name: "CVV", 591 | size: 3, 592 | }); 593 | 594 | expect(creditCardType("4")[0].niceType).toEqual("Fancy Visa"); 595 | }); 596 | 597 | it("can update pattern", () => { 598 | creditCardType.updateCard(creditCardType.types.VISA, { 599 | patterns: [3], 600 | }); 601 | 602 | expect(creditCardType("3")[0].type).toBe("visa"); 603 | }); 604 | 605 | it("can update more than once", () => { 606 | let updatedVisa; 607 | 608 | creditCardType.updateCard(creditCardType.types.VISA, { 609 | lengths: [11], 610 | }); 611 | 612 | updatedVisa = creditCardType.getTypeInfo(creditCardType.types.VISA); 613 | 614 | expect(updatedVisa.lengths).toEqual([11]); 615 | expect(updatedVisa.niceType).toBe("Visa"); 616 | 617 | creditCardType.updateCard(creditCardType.types.VISA, { 618 | niceType: "Fancy Visa", 619 | }); 620 | 621 | updatedVisa = creditCardType.getTypeInfo(creditCardType.types.VISA); 622 | 623 | expect(updatedVisa.niceType).toBe("Fancy Visa"); 624 | expect(updatedVisa.lengths).toEqual([11]); 625 | }); 626 | 627 | it("can update custom cards", () => { 628 | let card; 629 | 630 | creditCardType.addCard({ 631 | niceType: "NewCard", 632 | type: "new-card", 633 | patterns: [2345], 634 | gaps: [4, 8, 12], 635 | lengths: [16], 636 | code: { 637 | name: "cvv", 638 | size: 3, 639 | }, 640 | }); 641 | 642 | card = creditCardType.getTypeInfo("new-card"); 643 | 644 | expect(card.niceType).toBe("NewCard"); 645 | 646 | creditCardType.updateCard(card.type, { 647 | niceType: "Fancy NewCard", 648 | }); 649 | 650 | card = creditCardType.getTypeInfo("new-card"); 651 | 652 | expect(card.niceType).toBe("Fancy NewCard"); 653 | }); 654 | }); 655 | 656 | describe("changeOrder", () => { 657 | afterEach(() => { 658 | creditCardType.resetModifications(); 659 | }); 660 | 661 | it("changes test order priority", () => { 662 | creditCardType.changeOrder("jcb", 0); 663 | 664 | const result = creditCardType("2"); 665 | 666 | expect(result).toHaveLength(3); 667 | expect(result[0].type).toBe("jcb"); 668 | expect(result[1].type).toBe("mastercard"); 669 | expect(result[2].type).toBe("mir"); 670 | }); 671 | 672 | it("throws an error if card type is passed which is not in the array", () => { 673 | expect(() => { 674 | creditCardType.changeOrder("bogus", 0); 675 | }).toThrowError('"bogus" is not a supported card type.'); 676 | }); 677 | }); 678 | 679 | describe("types", () => { 680 | it("corresponds to internal type codes", () => { 681 | const exposedTypes = Object.keys(creditCardType.types).map( 682 | (key: string) => creditCardType.types[key], 683 | ); 684 | const internalTypes = creditCardType("").map((entry) => entry.type); 685 | 686 | expect(exposedTypes).toEqual(internalTypes); 687 | }); 688 | }); 689 | --------------------------------------------------------------------------------