2018-05-15 19:41:35 +03:00
|
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
|
|
|
|
"use strict";
|
|
|
|
|
2021-04-01 17:51:47 +03:00
|
|
|
var EXPORTED_SYMBOLS = ["CreditCard"];
|
2018-05-15 19:41:35 +03:00
|
|
|
|
2018-08-29 01:59:57 +03:00
|
|
|
// The list of known and supported credit card network ids ("types")
|
|
|
|
// This list mirrors the networks from dom/payments/BasicCardPayment.cpp
|
|
|
|
// and is defined by https://www.w3.org/Payments/card-network-ids
|
|
|
|
const SUPPORTED_NETWORKS = Object.freeze([
|
|
|
|
"amex",
|
|
|
|
"cartebancaire",
|
|
|
|
"diners",
|
|
|
|
"discover",
|
|
|
|
"jcb",
|
|
|
|
"mastercard",
|
|
|
|
"mir",
|
|
|
|
"unionpay",
|
|
|
|
"visa",
|
|
|
|
]);
|
|
|
|
|
2020-07-02 18:36:44 +03:00
|
|
|
// This lists stores lower cased variations of popular credit card network
|
|
|
|
// names for matching against strings.
|
|
|
|
const NETWORK_NAMES = {
|
|
|
|
"american express": "amex",
|
|
|
|
"master card": "mastercard",
|
|
|
|
"union pay": "unionpay",
|
|
|
|
};
|
|
|
|
|
2020-06-22 19:06:14 +03:00
|
|
|
// Based on https://en.wikipedia.org/wiki/Payment_card_number
|
|
|
|
//
|
|
|
|
// Notice:
|
|
|
|
// - CarteBancaire (`4035`, `4360`) is now recognized as Visa.
|
|
|
|
// - UnionPay (`63--`) is now recognized as Discover.
|
|
|
|
// This means that the order matters.
|
|
|
|
// First we'll try to match more specific card,
|
|
|
|
// and if that doesn't match we'll test against the more generic range.
|
|
|
|
const CREDIT_CARD_IIN = [
|
2020-07-11 01:46:49 +03:00
|
|
|
{ type: "amex", start: 34, end: 34, len: 15 },
|
|
|
|
{ type: "amex", start: 37, end: 37, len: 15 },
|
|
|
|
{ type: "cartebancaire", start: 4035, end: 4035, len: 16 },
|
|
|
|
{ type: "cartebancaire", start: 4360, end: 4360, len: 16 },
|
|
|
|
// We diverge from Wikipedia here, because Diners card
|
|
|
|
// support length of 14-19.
|
|
|
|
{ type: "diners", start: 300, end: 305, len: [14, 19] },
|
|
|
|
{ type: "diners", start: 3095, end: 3095, len: [14, 19] },
|
|
|
|
{ type: "diners", start: 36, end: 36, len: [14, 19] },
|
|
|
|
{ type: "diners", start: 38, end: 39, len: [14, 19] },
|
|
|
|
{ type: "discover", start: 6011, end: 6011, len: [16, 19] },
|
|
|
|
{ type: "discover", start: 622126, end: 622925, len: [16, 19] },
|
|
|
|
{ type: "discover", start: 624000, end: 626999, len: [16, 19] },
|
|
|
|
{ type: "discover", start: 628200, end: 628899, len: [16, 19] },
|
|
|
|
{ type: "discover", start: 64, end: 65, len: [16, 19] },
|
|
|
|
{ type: "jcb", start: 3528, end: 3589, len: [16, 19] },
|
|
|
|
{ type: "mastercard", start: 2221, end: 2720, len: 16 },
|
|
|
|
{ type: "mastercard", start: 51, end: 55, len: 16 },
|
|
|
|
{ type: "mir", start: 2200, end: 2204, len: 16 },
|
|
|
|
{ type: "unionpay", start: 62, end: 62, len: [16, 19] },
|
|
|
|
{ type: "unionpay", start: 81, end: 81, len: [16, 19] },
|
|
|
|
{ type: "visa", start: 4, end: 4, len: 16 },
|
2020-06-22 19:06:14 +03:00
|
|
|
].sort((a, b) => b.start - a.start);
|
|
|
|
|
2018-05-15 19:41:35 +03:00
|
|
|
class CreditCard {
|
|
|
|
/**
|
2018-11-13 20:35:29 +03:00
|
|
|
* A CreditCard object represents a credit card, with
|
|
|
|
* number, name, expiration, network, and CCV.
|
|
|
|
* The number is the only required information when creating
|
|
|
|
* an object, all other members are optional. The number
|
|
|
|
* is validated during construction and will throw if invalid.
|
|
|
|
*
|
|
|
|
* @param {string} name, optional
|
2018-05-15 19:41:35 +03:00
|
|
|
* @param {string} number
|
2018-11-13 20:35:29 +03:00
|
|
|
* @param {string} expirationString, optional
|
|
|
|
* @param {string|number} expirationMonth, optional
|
|
|
|
* @param {string|number} expirationYear, optional
|
|
|
|
* @param {string} network, optional
|
|
|
|
* @param {string|number} ccv, optional
|
|
|
|
* @param {string} encryptedNumber, optional
|
|
|
|
* @throws if number is an invalid credit card number
|
2018-05-15 19:41:35 +03:00
|
|
|
*/
|
|
|
|
constructor({
|
|
|
|
name,
|
|
|
|
number,
|
|
|
|
expirationString,
|
|
|
|
expirationMonth,
|
|
|
|
expirationYear,
|
2018-08-29 01:59:57 +03:00
|
|
|
network,
|
2018-05-15 19:41:35 +03:00
|
|
|
ccv,
|
|
|
|
encryptedNumber,
|
|
|
|
}) {
|
|
|
|
this._name = name;
|
|
|
|
this._unmodifiedNumber = number;
|
|
|
|
this._encryptedNumber = encryptedNumber;
|
|
|
|
this._ccv = ccv;
|
|
|
|
this.number = number;
|
2018-11-13 20:35:29 +03:00
|
|
|
let { month, year } = CreditCard.normalizeExpiration({
|
|
|
|
expirationString,
|
|
|
|
expirationMonth,
|
|
|
|
expirationYear,
|
|
|
|
});
|
|
|
|
this._expirationMonth = month;
|
|
|
|
this._expirationYear = year;
|
|
|
|
this.network = network;
|
2018-05-15 19:41:35 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
set name(value) {
|
|
|
|
this._name = value;
|
|
|
|
}
|
|
|
|
|
|
|
|
set expirationMonth(value) {
|
|
|
|
if (typeof value == "undefined") {
|
|
|
|
this._expirationMonth = undefined;
|
|
|
|
return;
|
|
|
|
}
|
2018-11-13 20:35:29 +03:00
|
|
|
this._expirationMonth = CreditCard.normalizeExpirationMonth(value);
|
2018-05-15 19:41:35 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
get expirationMonth() {
|
|
|
|
return this._expirationMonth;
|
|
|
|
}
|
|
|
|
|
|
|
|
set expirationYear(value) {
|
|
|
|
if (typeof value == "undefined") {
|
|
|
|
this._expirationYear = undefined;
|
|
|
|
return;
|
|
|
|
}
|
2018-11-13 20:35:29 +03:00
|
|
|
this._expirationYear = CreditCard.normalizeExpirationYear(value);
|
2018-05-15 19:41:35 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
get expirationYear() {
|
|
|
|
return this._expirationYear;
|
|
|
|
}
|
|
|
|
|
|
|
|
set expirationString(value) {
|
2018-11-13 20:35:29 +03:00
|
|
|
let { month, year } = CreditCard.parseExpirationString(value);
|
2018-05-15 19:41:35 +03:00
|
|
|
this.expirationMonth = month;
|
|
|
|
this.expirationYear = year;
|
|
|
|
}
|
|
|
|
|
|
|
|
set ccv(value) {
|
|
|
|
this._ccv = value;
|
|
|
|
}
|
|
|
|
|
|
|
|
get number() {
|
|
|
|
return this._number;
|
|
|
|
}
|
|
|
|
|
2018-11-13 20:35:29 +03:00
|
|
|
/**
|
|
|
|
* Sets the number member of a CreditCard object. If the number
|
|
|
|
* is not valid according to the Luhn algorithm then the member
|
|
|
|
* will get set to the empty string before throwing an exception.
|
|
|
|
*
|
|
|
|
* @param {string} value
|
|
|
|
* @throws if the value is an invalid credit card number
|
|
|
|
*/
|
2018-05-15 19:41:35 +03:00
|
|
|
set number(value) {
|
|
|
|
if (value) {
|
|
|
|
let normalizedNumber = value.replace(/[-\s]/g, "");
|
|
|
|
// Based on the information on wiki[1], the shortest valid length should be
|
2018-10-16 08:37:39 +03:00
|
|
|
// 12 digits (Maestro).
|
|
|
|
// [1] https://en.wikipedia.org/wiki/Payment_card_number
|
|
|
|
normalizedNumber = normalizedNumber.match(/^\d{12,}$/)
|
2018-11-13 20:35:29 +03:00
|
|
|
? normalizedNumber
|
|
|
|
: "";
|
2018-05-15 19:41:35 +03:00
|
|
|
this._number = normalizedNumber;
|
2018-11-13 20:35:29 +03:00
|
|
|
} else {
|
|
|
|
this._number = "";
|
|
|
|
}
|
|
|
|
|
|
|
|
if (value && !this.isValidNumber()) {
|
|
|
|
this._number = "";
|
|
|
|
throw new Error("Invalid credit card number");
|
2018-05-15 19:41:35 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-29 01:59:57 +03:00
|
|
|
get network() {
|
|
|
|
return this._network;
|
|
|
|
}
|
|
|
|
|
|
|
|
set network(value) {
|
|
|
|
this._network = value || undefined;
|
|
|
|
}
|
|
|
|
|
2018-05-15 19:41:35 +03:00
|
|
|
// Implements the Luhn checksum algorithm as described at
|
|
|
|
// http://wikipedia.org/wiki/Luhn_algorithm
|
2018-10-16 08:37:39 +03:00
|
|
|
// Number digit lengths vary with network, but should fall within 12-19 range. [2]
|
|
|
|
// More details at https://en.wikipedia.org/wiki/Payment_card_number
|
2018-05-15 19:41:35 +03:00
|
|
|
isValidNumber() {
|
|
|
|
if (!this._number) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove dashes and whitespace
|
|
|
|
let number = this._number.replace(/[\-\s]/g, "");
|
|
|
|
|
|
|
|
let len = number.length;
|
2018-10-16 08:37:39 +03:00
|
|
|
if (len < 12 || len > 19) {
|
2018-05-15 19:41:35 +03:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!/^\d+$/.test(number)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
let total = 0;
|
|
|
|
for (let i = 0; i < len; i++) {
|
|
|
|
let ch = parseInt(number[len - i - 1], 10);
|
|
|
|
if (i % 2 == 1) {
|
|
|
|
// Double it, add digits together if > 10
|
|
|
|
ch *= 2;
|
|
|
|
if (ch > 9) {
|
|
|
|
ch -= 9;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
total += ch;
|
|
|
|
}
|
|
|
|
return total % 10 == 0;
|
|
|
|
}
|
|
|
|
|
2020-06-22 19:06:14 +03:00
|
|
|
/**
|
|
|
|
* Attempts to match the number against known network identifiers.
|
|
|
|
*
|
|
|
|
* @param {string} ccNumber
|
|
|
|
*
|
|
|
|
* @returns {string|null}
|
|
|
|
*/
|
|
|
|
static getType(ccNumber) {
|
|
|
|
for (let i = 0; i < CREDIT_CARD_IIN.length; i++) {
|
|
|
|
const range = CREDIT_CARD_IIN[i];
|
2020-07-11 01:46:49 +03:00
|
|
|
if (typeof range.len == "number") {
|
|
|
|
if (range.len != ccNumber.length) {
|
|
|
|
continue;
|
2020-06-22 19:06:14 +03:00
|
|
|
}
|
2020-07-11 01:46:49 +03:00
|
|
|
} else if (
|
|
|
|
ccNumber.length < range.len[0] ||
|
|
|
|
ccNumber.length > range.len[1]
|
|
|
|
) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
const prefixLength = Math.floor(Math.log10(range.start)) + 1;
|
|
|
|
const prefix = parseInt(ccNumber.substring(0, prefixLength), 10);
|
|
|
|
if (prefix >= range.start && prefix <= range.end) {
|
|
|
|
return range.type;
|
2020-06-22 19:06:14 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2020-07-02 18:36:44 +03:00
|
|
|
/**
|
|
|
|
* Attempts to retrieve a card network identifier based
|
|
|
|
* on a name.
|
|
|
|
*
|
|
|
|
* @param {string|undefined|null} name
|
|
|
|
*
|
|
|
|
* @returns {string|null}
|
|
|
|
*/
|
|
|
|
static getNetworkFromName(name) {
|
|
|
|
if (!name) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
let lcName = name
|
|
|
|
.trim()
|
|
|
|
.toLowerCase()
|
|
|
|
.normalize("NFKC");
|
|
|
|
if (SUPPORTED_NETWORKS.includes(lcName)) {
|
|
|
|
return lcName;
|
|
|
|
}
|
|
|
|
for (let term in NETWORK_NAMES) {
|
|
|
|
if (lcName.includes(term)) {
|
|
|
|
return NETWORK_NAMES[term];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2018-05-15 19:41:35 +03:00
|
|
|
/**
|
|
|
|
* Returns true if the card number is valid and the
|
|
|
|
* expiration date has not passed. Otherwise false.
|
|
|
|
*
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
|
|
|
isValid() {
|
|
|
|
if (!this.isValidNumber()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
let currentDate = new Date();
|
|
|
|
let currentYear = currentDate.getFullYear();
|
|
|
|
if (this._expirationYear > currentYear) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// getMonth is 0-based, so add 1 because credit cards are 1-based
|
|
|
|
let currentMonth = currentDate.getMonth() + 1;
|
|
|
|
return (
|
|
|
|
this._expirationYear == currentYear &&
|
|
|
|
this._expirationMonth >= currentMonth
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
get maskedNumber() {
|
2018-11-13 20:35:29 +03:00
|
|
|
return CreditCard.getMaskedNumber(this._number);
|
2018-05-15 19:41:35 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
get longMaskedNumber() {
|
2018-11-13 20:35:29 +03:00
|
|
|
return CreditCard.getLongMaskedNumber(this._number);
|
2018-05-15 19:41:35 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-06-08 18:00:58 +03:00
|
|
|
* Get credit card display label. It should display masked numbers, the
|
|
|
|
* cardholder's name, and the expiration date, separated by a commas.
|
2020-09-11 09:13:36 +03:00
|
|
|
* In addition, the card type is provided in the accessibility label.
|
2020-06-08 18:00:58 +03:00
|
|
|
*/
|
2020-09-11 09:13:36 +03:00
|
|
|
static getLabelInfo({ number, name, month, year, type }) {
|
2020-06-08 18:00:58 +03:00
|
|
|
let formatSelector = ["number"];
|
|
|
|
if (name) {
|
|
|
|
formatSelector.push("name");
|
|
|
|
}
|
|
|
|
if (month && year) {
|
|
|
|
formatSelector.push("expiration");
|
|
|
|
}
|
2020-09-11 09:13:36 +03:00
|
|
|
let stringId = `credit-card-label-${formatSelector.join("-")}-2`;
|
2020-06-08 18:00:58 +03:00
|
|
|
return {
|
|
|
|
id: stringId,
|
|
|
|
args: {
|
|
|
|
number: CreditCard.getMaskedNumber(number),
|
|
|
|
name,
|
|
|
|
month: month?.toString(),
|
|
|
|
year: year?.toString(),
|
2020-09-11 09:13:36 +03:00
|
|
|
type,
|
2020-06-08 18:00:58 +03:00
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* !!! DEPRECATED !!!
|
|
|
|
* Please use getLabelInfo above, as it allows for localization.
|
2018-05-15 19:41:35 +03:00
|
|
|
*/
|
2018-11-13 20:35:29 +03:00
|
|
|
static getLabel({ number, name }) {
|
2018-05-15 19:41:35 +03:00
|
|
|
let parts = [];
|
|
|
|
|
2018-11-13 20:35:29 +03:00
|
|
|
if (number) {
|
|
|
|
parts.push(CreditCard.getMaskedNumber(number));
|
2018-05-15 19:41:35 +03:00
|
|
|
}
|
2018-11-13 20:35:29 +03:00
|
|
|
if (name) {
|
|
|
|
parts.push(name);
|
2018-05-15 19:41:35 +03:00
|
|
|
}
|
|
|
|
return parts.join(", ");
|
|
|
|
}
|
|
|
|
|
2018-11-13 20:35:29 +03:00
|
|
|
static normalizeExpirationMonth(month) {
|
2018-05-15 19:41:35 +03:00
|
|
|
month = parseInt(month, 10);
|
|
|
|
if (isNaN(month) || month < 1 || month > 12) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
return month;
|
|
|
|
}
|
|
|
|
|
2018-11-13 20:35:29 +03:00
|
|
|
static normalizeExpirationYear(year) {
|
2018-05-15 19:41:35 +03:00
|
|
|
year = parseInt(year, 10);
|
|
|
|
if (isNaN(year) || year < 0) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
if (year < 100) {
|
|
|
|
year += 2000;
|
|
|
|
}
|
|
|
|
return year;
|
|
|
|
}
|
|
|
|
|
2018-11-13 20:35:29 +03:00
|
|
|
static parseExpirationString(expirationString) {
|
2018-05-15 19:41:35 +03:00
|
|
|
let rules = [
|
|
|
|
{
|
|
|
|
regex: "(\\d{4})[-/](\\d{1,2})",
|
|
|
|
yearIndex: 1,
|
|
|
|
monthIndex: 2,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
regex: "(\\d{1,2})[-/](\\d{4})",
|
|
|
|
yearIndex: 2,
|
|
|
|
monthIndex: 1,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
regex: "(\\d{1,2})[-/](\\d{1,2})",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
regex: "(\\d{2})(\\d{2})",
|
|
|
|
},
|
|
|
|
];
|
|
|
|
|
|
|
|
for (let rule of rules) {
|
|
|
|
let result = new RegExp(`(?:^|\\D)${rule.regex}(?!\\d)`).exec(
|
|
|
|
expirationString
|
|
|
|
);
|
|
|
|
if (!result) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
let year, month;
|
|
|
|
|
|
|
|
if (!rule.yearIndex || !rule.monthIndex) {
|
|
|
|
month = parseInt(result[1], 10);
|
|
|
|
if (month > 12) {
|
|
|
|
year = parseInt(result[1], 10);
|
|
|
|
month = parseInt(result[2], 10);
|
|
|
|
} else {
|
|
|
|
year = parseInt(result[2], 10);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
year = parseInt(result[rule.yearIndex], 10);
|
|
|
|
month = parseInt(result[rule.monthIndex], 10);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (month < 1 || month > 12 || (year >= 100 && year < 2000)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
return { month, year };
|
|
|
|
}
|
|
|
|
return { month: undefined, year: undefined };
|
|
|
|
}
|
|
|
|
|
2018-11-13 20:35:29 +03:00
|
|
|
static normalizeExpiration({
|
|
|
|
expirationString,
|
|
|
|
expirationMonth,
|
|
|
|
expirationYear,
|
|
|
|
}) {
|
|
|
|
// Only prefer the string version if missing one or both parsed formats.
|
|
|
|
let parsedExpiration = {};
|
|
|
|
if (expirationString && (!expirationMonth || !expirationYear)) {
|
|
|
|
parsedExpiration = CreditCard.parseExpirationString(expirationString);
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
month: CreditCard.normalizeExpirationMonth(
|
|
|
|
parsedExpiration.month || expirationMonth
|
|
|
|
),
|
|
|
|
year: CreditCard.normalizeExpirationYear(
|
|
|
|
parsedExpiration.year || expirationYear
|
|
|
|
),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2018-05-15 19:41:35 +03:00
|
|
|
static formatMaskedNumber(maskedNumber) {
|
|
|
|
return {
|
|
|
|
affix: "****",
|
|
|
|
label: maskedNumber.replace(/^\**/, ""),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
static getMaskedNumber(number) {
|
2018-11-13 20:35:29 +03:00
|
|
|
return "*".repeat(4) + " " + number.substr(-4);
|
2018-05-15 19:41:35 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
static getLongMaskedNumber(number) {
|
2018-11-13 20:35:29 +03:00
|
|
|
return "*".repeat(number.length - 4) + number.substr(-4);
|
2018-05-15 19:41:35 +03:00
|
|
|
}
|
|
|
|
|
2018-11-13 20:35:29 +03:00
|
|
|
/*
|
|
|
|
* Validates the number according to the Luhn algorithm. This
|
|
|
|
* method does not throw an exception if the number is invalid.
|
|
|
|
*/
|
2018-05-15 19:41:35 +03:00
|
|
|
static isValidNumber(number) {
|
2018-11-13 20:35:29 +03:00
|
|
|
try {
|
|
|
|
new CreditCard({ number });
|
|
|
|
} catch (ex) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
2018-05-15 19:41:35 +03:00
|
|
|
}
|
2018-08-29 01:59:57 +03:00
|
|
|
|
|
|
|
static isValidNetwork(network) {
|
|
|
|
return SUPPORTED_NETWORKS.includes(network);
|
|
|
|
}
|
2018-05-15 19:41:35 +03:00
|
|
|
}
|
2018-08-29 01:59:57 +03:00
|
|
|
CreditCard.SUPPORTED_NETWORKS = SUPPORTED_NETWORKS;
|