forked from mirrors/gecko-dev
CLOSED TREE Backed out changeset 7b19f4ed5182 (bug 1681985) Backed out changeset e77582268ce1 (bug 1681985) Backed out changeset 386a8b11c127 (bug 1681985)
477 lines
12 KiB
JavaScript
477 lines
12 KiB
JavaScript
/* 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";
|
|
|
|
var EXPORTED_SYMBOLS = ["CreditCard"];
|
|
|
|
// 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",
|
|
]);
|
|
|
|
// 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",
|
|
};
|
|
|
|
// 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 = [
|
|
{ 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 },
|
|
].sort((a, b) => b.start - a.start);
|
|
|
|
class CreditCard {
|
|
/**
|
|
* 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
|
|
* @param {string} number
|
|
* @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
|
|
*/
|
|
constructor({
|
|
name,
|
|
number,
|
|
expirationString,
|
|
expirationMonth,
|
|
expirationYear,
|
|
network,
|
|
ccv,
|
|
encryptedNumber,
|
|
}) {
|
|
this._name = name;
|
|
this._unmodifiedNumber = number;
|
|
this._encryptedNumber = encryptedNumber;
|
|
this._ccv = ccv;
|
|
this.number = number;
|
|
let { month, year } = CreditCard.normalizeExpiration({
|
|
expirationString,
|
|
expirationMonth,
|
|
expirationYear,
|
|
});
|
|
this._expirationMonth = month;
|
|
this._expirationYear = year;
|
|
this.network = network;
|
|
}
|
|
|
|
set name(value) {
|
|
this._name = value;
|
|
}
|
|
|
|
set expirationMonth(value) {
|
|
if (typeof value == "undefined") {
|
|
this._expirationMonth = undefined;
|
|
return;
|
|
}
|
|
this._expirationMonth = CreditCard.normalizeExpirationMonth(value);
|
|
}
|
|
|
|
get expirationMonth() {
|
|
return this._expirationMonth;
|
|
}
|
|
|
|
set expirationYear(value) {
|
|
if (typeof value == "undefined") {
|
|
this._expirationYear = undefined;
|
|
return;
|
|
}
|
|
this._expirationYear = CreditCard.normalizeExpirationYear(value);
|
|
}
|
|
|
|
get expirationYear() {
|
|
return this._expirationYear;
|
|
}
|
|
|
|
set expirationString(value) {
|
|
let { month, year } = CreditCard.parseExpirationString(value);
|
|
this.expirationMonth = month;
|
|
this.expirationYear = year;
|
|
}
|
|
|
|
set ccv(value) {
|
|
this._ccv = value;
|
|
}
|
|
|
|
get number() {
|
|
return this._number;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
set number(value) {
|
|
if (value) {
|
|
let normalizedNumber = value.replace(/[-\s]/g, "");
|
|
// Based on the information on wiki[1], the shortest valid length should be
|
|
// 12 digits (Maestro).
|
|
// [1] https://en.wikipedia.org/wiki/Payment_card_number
|
|
normalizedNumber = normalizedNumber.match(/^\d{12,}$/)
|
|
? normalizedNumber
|
|
: "";
|
|
this._number = normalizedNumber;
|
|
} else {
|
|
this._number = "";
|
|
}
|
|
|
|
if (value && !this.isValidNumber()) {
|
|
this._number = "";
|
|
throw new Error("Invalid credit card number");
|
|
}
|
|
}
|
|
|
|
get network() {
|
|
return this._network;
|
|
}
|
|
|
|
set network(value) {
|
|
this._network = value || undefined;
|
|
}
|
|
|
|
// Implements the Luhn checksum algorithm as described at
|
|
// http://wikipedia.org/wiki/Luhn_algorithm
|
|
// 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
|
|
isValidNumber() {
|
|
if (!this._number) {
|
|
return false;
|
|
}
|
|
|
|
// Remove dashes and whitespace
|
|
let number = this._number.replace(/[\-\s]/g, "");
|
|
|
|
let len = number.length;
|
|
if (len < 12 || len > 19) {
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* 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];
|
|
if (typeof range.len == "number") {
|
|
if (range.len != ccNumber.length) {
|
|
continue;
|
|
}
|
|
} 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;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* 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() {
|
|
return CreditCard.getMaskedNumber(this._number);
|
|
}
|
|
|
|
get longMaskedNumber() {
|
|
return CreditCard.getLongMaskedNumber(this._number);
|
|
}
|
|
|
|
/**
|
|
* Get credit card display label. It should display masked numbers, the
|
|
* cardholder's name, and the expiration date, separated by a commas.
|
|
* In addition, the card type is provided in the accessibility label.
|
|
*/
|
|
static getLabelInfo({ number, name, month, year, type }) {
|
|
let formatSelector = ["number"];
|
|
if (name) {
|
|
formatSelector.push("name");
|
|
}
|
|
if (month && year) {
|
|
formatSelector.push("expiration");
|
|
}
|
|
let stringId = `credit-card-label-${formatSelector.join("-")}-2`;
|
|
return {
|
|
id: stringId,
|
|
args: {
|
|
number: CreditCard.getMaskedNumber(number),
|
|
name,
|
|
month: month?.toString(),
|
|
year: year?.toString(),
|
|
type,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* !!! DEPRECATED !!!
|
|
* Please use getLabelInfo above, as it allows for localization.
|
|
*/
|
|
static getLabel({ number, name }) {
|
|
let parts = [];
|
|
|
|
if (number) {
|
|
parts.push(CreditCard.getMaskedNumber(number));
|
|
}
|
|
if (name) {
|
|
parts.push(name);
|
|
}
|
|
return parts.join(", ");
|
|
}
|
|
|
|
static normalizeExpirationMonth(month) {
|
|
month = parseInt(month, 10);
|
|
if (isNaN(month) || month < 1 || month > 12) {
|
|
return undefined;
|
|
}
|
|
return month;
|
|
}
|
|
|
|
static normalizeExpirationYear(year) {
|
|
year = parseInt(year, 10);
|
|
if (isNaN(year) || year < 0) {
|
|
return undefined;
|
|
}
|
|
if (year < 100) {
|
|
year += 2000;
|
|
}
|
|
return year;
|
|
}
|
|
|
|
static parseExpirationString(expirationString) {
|
|
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 };
|
|
}
|
|
|
|
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
|
|
),
|
|
};
|
|
}
|
|
|
|
static formatMaskedNumber(maskedNumber) {
|
|
return {
|
|
affix: "****",
|
|
label: maskedNumber.replace(/^\**/, ""),
|
|
};
|
|
}
|
|
|
|
static getMaskedNumber(number) {
|
|
return "*".repeat(4) + " " + number.substr(-4);
|
|
}
|
|
|
|
static getLongMaskedNumber(number) {
|
|
return "*".repeat(number.length - 4) + number.substr(-4);
|
|
}
|
|
|
|
/*
|
|
* Validates the number according to the Luhn algorithm. This
|
|
* method does not throw an exception if the number is invalid.
|
|
*/
|
|
static isValidNumber(number) {
|
|
try {
|
|
new CreditCard({ number });
|
|
} catch (ex) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static isValidNetwork(network) {
|
|
return SUPPORTED_NETWORKS.includes(network);
|
|
}
|
|
}
|
|
CreditCard.SUPPORTED_NETWORKS = SUPPORTED_NETWORKS;
|