1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 // The list of known and supported credit card network ids ("types")
6 // This list mirrors the networks from dom/payments/BasicCardPayment.cpp
7 // and is defined by https://www.w3.org/Payments/card-network-ids
8 const SUPPORTED_NETWORKS = Object.freeze([
20 // This lists stores lower cased variations of popular credit card network
21 // names for matching against strings.
22 export const NETWORK_NAMES = {
23 "american express": "amex",
24 "master card": "mastercard",
25 "union pay": "unionpay",
28 // Based on https://en.wikipedia.org/wiki/Payment_card_number
31 // - CarteBancaire (`4035`, `4360`) is now recognized as Visa.
32 // - UnionPay (`63--`) is now recognized as Discover.
33 // This means that the order matters.
34 // First we'll try to match more specific card,
35 // and if that doesn't match we'll test against the more generic range.
36 const CREDIT_CARD_IIN = [
37 { type: "amex", start: 34, end: 34, len: 15 },
38 { type: "amex", start: 37, end: 37, len: 15 },
39 { type: "cartebancaire", start: 4035, end: 4035, len: 16 },
40 { type: "cartebancaire", start: 4360, end: 4360, len: 16 },
41 // We diverge from Wikipedia here, because Diners card
42 // support length of 14-19.
43 { type: "diners", start: 300, end: 305, len: [14, 19] },
44 { type: "diners", start: 3095, end: 3095, len: [14, 19] },
45 { type: "diners", start: 36, end: 36, len: [14, 19] },
46 { type: "diners", start: 38, end: 39, len: [14, 19] },
47 { type: "discover", start: 6011, end: 6011, len: [16, 19] },
48 { type: "discover", start: 622126, end: 622925, len: [16, 19] },
49 { type: "discover", start: 624000, end: 626999, len: [16, 19] },
50 { type: "discover", start: 628200, end: 628899, len: [16, 19] },
51 { type: "discover", start: 64, end: 65, len: [16, 19] },
52 { type: "jcb", start: 3528, end: 3589, len: [16, 19] },
53 { type: "mastercard", start: 2221, end: 2720, len: 16 },
54 { type: "mastercard", start: 51, end: 55, len: 16 },
55 { type: "mir", start: 2200, end: 2204, len: 16 },
56 { type: "unionpay", start: 62, end: 62, len: [16, 19] },
57 { type: "unionpay", start: 81, end: 81, len: [16, 19] },
58 { type: "visa", start: 4, end: 4, len: 16 },
59 ].sort((a, b) => b.start - a.start);
61 export class CreditCard {
63 * A CreditCard object represents a credit card, with
64 * number, name, expiration, network, and CCV.
65 * The number is the only required information when creating
66 * an object, all other members are optional. The number
67 * is validated during construction and will throw if invalid.
69 * @param {string} name, optional
70 * @param {string} number
71 * @param {string} expirationString, optional
72 * @param {string|number} expirationMonth, optional
73 * @param {string|number} expirationYear, optional
74 * @param {string} network, optional
75 * @param {string|number} ccv, optional
76 * @param {string} encryptedNumber, optional
77 * @throws if number is an invalid credit card number
90 this._unmodifiedNumber = number;
91 this._encryptedNumber = encryptedNumber;
94 let { month, year } = CreditCard.normalizeExpiration({
99 this._expirationMonth = month;
100 this._expirationYear = year;
101 this.network = network;
108 set expirationMonth(value) {
109 if (typeof value == "undefined") {
110 this._expirationMonth = undefined;
113 this._expirationMonth = CreditCard.normalizeExpirationMonth(value);
116 get expirationMonth() {
117 return this._expirationMonth;
120 set expirationYear(value) {
121 if (typeof value == "undefined") {
122 this._expirationYear = undefined;
125 this._expirationYear = CreditCard.normalizeExpirationYear(value);
128 get expirationYear() {
129 return this._expirationYear;
132 set expirationString(value) {
133 let { month, year } = CreditCard.parseExpirationString(value);
134 this.expirationMonth = month;
135 this.expirationYear = year;
147 * Sets the number member of a CreditCard object. If the number
148 * is not valid according to the Luhn algorithm then the member
149 * will get set to the empty string before throwing an exception.
151 * @param {string} value
152 * @throws if the value is an invalid credit card number
156 let normalizedNumber = CreditCard.normalizeCardNumber(value);
157 // Based on the information on wiki[1], the shortest valid length should be
158 // 12 digits (Maestro).
159 // [1] https://en.wikipedia.org/wiki/Payment_card_number
160 normalizedNumber = normalizedNumber.match(/^\d{12,}$/)
163 this._number = normalizedNumber;
168 if (value && !this.isValidNumber()) {
170 throw new Error("Invalid credit card number");
175 return this._network;
179 this._network = value || undefined;
182 // Implements the Luhn checksum algorithm as described at
183 // http://wikipedia.org/wiki/Luhn_algorithm
184 // Number digit lengths vary with network, but should fall within 12-19 range. [2]
185 // More details at https://en.wikipedia.org/wiki/Payment_card_number
191 // Remove dashes and whitespace
192 const number = CreditCard.normalizeCardNumber(this._number);
194 const len = number.length;
195 if (len < 12 || len > 19) {
199 if (!/^\d+$/.test(number)) {
204 for (let i = 0; i < len; i++) {
205 let ch = parseInt(number[len - i - 1], 10);
207 // Double it, add digits together if > 10
215 return total % 10 == 0;
219 * Normalizes a credit card number.
220 * @param {string} number
221 * @return {string | null}
222 * @memberof CreditCard
224 static normalizeCardNumber(number) {
228 return number.replace(/[\-\s]/g, "");
232 * Attempts to match the number against known network identifiers.
234 * @param {string} ccNumber Credit card number with no spaces or special characters in it.
236 * @returns {string|null}
238 static getType(ccNumber) {
243 for (let i = 0; i < CREDIT_CARD_IIN.length; i++) {
244 const range = CREDIT_CARD_IIN[i];
245 if (typeof range.len == "number") {
246 if (range.len != ccNumber.length) {
250 ccNumber.length < range.len[0] ||
251 ccNumber.length > range.len[1]
256 const prefixLength = Math.floor(Math.log10(range.start)) + 1;
257 const prefix = parseInt(ccNumber.substring(0, prefixLength), 10);
258 if (prefix >= range.start && prefix <= range.end) {
266 * Attempts to retrieve a card network identifier based
269 * @param {string|undefined|null} name
271 * @returns {string|null}
273 static getNetworkFromName(name) {
277 let lcName = name.trim().toLowerCase().normalize("NFKC");
278 if (SUPPORTED_NETWORKS.includes(lcName)) {
281 for (let term in NETWORK_NAMES) {
282 if (lcName.includes(term)) {
283 return NETWORK_NAMES[term];
290 * Returns true if the card number is valid and the
291 * expiration date has not passed. Otherwise false.
296 if (!this.isValidNumber()) {
300 let currentDate = new Date();
301 let currentYear = currentDate.getFullYear();
302 if (this._expirationYear > currentYear) {
306 // getMonth is 0-based, so add 1 because credit cards are 1-based
307 let currentMonth = currentDate.getMonth() + 1;
309 this._expirationYear == currentYear &&
310 this._expirationMonth >= currentMonth
315 return CreditCard.getMaskedNumber(this._number);
318 get longMaskedNumber() {
319 return CreditCard.getLongMaskedNumber(this._number);
323 * Get credit card display label. It should display masked numbers, the
324 * cardholder's name, and the expiration date, separated by a commas.
325 * In addition, the card type is provided in the accessibility label.
327 static getLabelInfo({ number, name, month, year, type }) {
328 let formatSelector = ["number"];
330 formatSelector.push("name");
333 formatSelector.push("expiration");
335 let stringId = `credit-card-label-${formatSelector.join("-")}-2`;
339 number: CreditCard.getMaskedNumber(number),
341 month: month?.toString(),
342 year: year?.toString(),
350 * Please use getLabelInfo above, as it allows for localization.
353 static getLabel({ number, name }) {
357 parts.push(CreditCard.getMaskedNumber(number));
362 return parts.join(", ");
365 static normalizeExpirationMonth(month) {
366 month = parseInt(month, 10);
367 if (isNaN(month) || month < 1 || month > 12) {
373 static normalizeExpirationYear(year) {
374 year = parseInt(year, 10);
375 if (isNaN(year) || year < 0) {
384 static parseExpirationString(expirationString) {
387 regex: /(?:^|\D)(\d{2})(\d{2})(?!\d)/,
390 regex: /(?:^|\D)(\d{4})[-/](\d{1,2})(?!\d)/,
395 regex: /(?:^|\D)(\d{1,2})[-/](\d{4})(?!\d)/,
400 regex: /(?:^|\D)(\d{1,2})[-/](\d{1,2})(?!\d)/,
403 regex: /(?:^|\D)(\d{2})(\d{2})(?!\d)/,
407 expirationString = expirationString.replaceAll(" ", "");
408 for (let rule of rules) {
409 let result = rule.regex.exec(expirationString);
415 const parsedResults = [parseInt(result[1], 10), parseInt(result[2], 10)];
416 if (!rule.yearIndex || !rule.monthIndex) {
417 month = parsedResults[0];
419 year = parsedResults[0];
420 month = parsedResults[1];
422 year = parsedResults[1];
425 year = parsedResults[rule.yearIndex];
426 month = parsedResults[rule.monthIndex];
429 if (month >= 1 && month <= 12 && (year < 100 || year > 2000)) {
430 return { month, year };
433 return { month: undefined, year: undefined };
436 static normalizeExpiration({
441 // Only prefer the string version if missing one or both parsed formats.
442 let parsedExpiration = {};
443 if (expirationString && (!expirationMonth || !expirationYear)) {
444 parsedExpiration = CreditCard.parseExpirationString(expirationString);
447 month: CreditCard.normalizeExpirationMonth(
448 parsedExpiration.month || expirationMonth
450 year: CreditCard.normalizeExpirationYear(
451 parsedExpiration.year || expirationYear
456 static formatMaskedNumber(maskedNumber) {
457 return "*".repeat(4) + maskedNumber.substr(-4);
460 static getMaskedNumber(number) {
461 return "*".repeat(4) + " " + number.substr(-4);
464 static getLongMaskedNumber(number) {
465 return "*".repeat(number.length - 4) + number.substr(-4);
468 static getCreditCardLogo(network) {
469 const PATH = "chrome://formautofill/content/";
470 const THIRD_PARTY_PATH = PATH + "third-party/";
473 return THIRD_PARTY_PATH + "cc-logo-amex.png";
474 case "cartebancaire":
475 return THIRD_PARTY_PATH + "cc-logo-cartebancaire.png";
477 return THIRD_PARTY_PATH + "cc-logo-diners.svg";
479 return THIRD_PARTY_PATH + "cc-logo-discover.png";
481 return THIRD_PARTY_PATH + "cc-logo-jcb.svg";
483 return THIRD_PARTY_PATH + "cc-logo-mastercard.svg";
485 return THIRD_PARTY_PATH + "cc-logo-mir.svg";
487 return THIRD_PARTY_PATH + "cc-logo-unionpay.svg";
489 return THIRD_PARTY_PATH + "cc-logo-visa.svg";
491 return PATH + "icon-credit-card-generic.svg";
496 * Validates the number according to the Luhn algorithm. This
497 * method does not throw an exception if the number is invalid.
499 static isValidNumber(number) {
501 new CreditCard({ number });
508 static isValidNetwork(network) {
509 return SUPPORTED_NETWORKS.includes(network);
512 static getSupportedNetworks() {
513 return SUPPORTED_NETWORKS;
517 * Localised names for supported networks are available in
518 * `browser/preferences/formAutofill.ftl`.
520 static getNetworkL10nId(network) {
521 return this.isValidNetwork(network)
522 ? `autofill-card-network-${network}`