@danports/cardswipe
Version:
Capture & parse data from magnetic stripe card readers.
268 lines (267 loc) • 10.7 kB
JavaScript
const e = {
cardSwipe: void 0,
settings: {},
init: function(t) {
return e.methods.init(t);
},
// Built-in parsers. These include simplistic credit card parsers that
// recognize various card issuers based on patterns of the account number.
// There is no guarantee these are correct or complete; they are based
// on information from Wikipedia.
// Account numbers are validated by the Luhn checksum algorithm.
builtinParsers: {
// Generic parser. Separates raw data into up to three lines.
generic: function(t) {
let s = new RegExp("^(%[^%;\\?]+\\?)?(;[0-9\\:<>\\=]+\\?)?([+;][0-9\\:<>\\=]+\\?)?").exec(t);
return s ? {
type: "generic",
line1: s[1] ? s[1].slice(1, -1) : "",
line2: s[2] ? s[2].slice(1, -1) : "",
line3: s[3] ? s[3].slice(1, -1) : ""
} : null;
},
// Visa card parser.
visa: function(t) {
let s = new RegExp("^%B(4[0-9]{12,18})\\^([A-Z ]+)/([A-Z ]+)(\\.[A-Z ]+)?\\^([0-9]{2})([0-9]{2})").exec(t);
if (!s) return null;
let r = s[1];
return e.luhnChecksum(r) ? {
type: "visa",
account: r,
lastName: s[2].trim(),
firstName: s[3].trim(),
honorific: s[4] ? s[4].trim().slice(1) : "",
expYear: s[5],
expMonth: s[6]
} : null;
},
// MasterCard parser.
mastercard: function(t) {
let s = new RegExp("^%B(5[1-5][0-9]{14})\\^([A-Z ]+)/([A-Z ]+)(\\.[A-Z ]+)?\\^([0-9]{2})([0-9]{2})").exec(t);
if (!s) return null;
let r = s[1];
return e.luhnChecksum(r) ? {
type: "mastercard",
account: r,
lastName: s[2],
firstName: s[3],
honorific: s[4] ? s[4].trim().slice(1) : "",
expYear: s[5],
expMonth: s[6]
} : null;
},
// Discover parser.
discover: function(t) {
let s = new RegExp("^%B(6[0-9]{15})\\^([A-Z ]+)/([A-Z ]+)(\\.[A-Z ]+)?\\^([0-9]{2})([0-9]{2})").exec(t);
if (!s) return null;
let r = s[1];
return e.luhnChecksum(r) ? {
type: "discover",
account: r,
lastName: s[2],
firstName: s[3],
honorific: s[4] ? s[4].trim().slice(1) : "",
expYear: s[5],
expMonth: s[6]
} : null;
},
// American Express parser
amex: function(t) {
let s = new RegExp("^%B(3[4|7][0-9]{13})\\^([A-Z ]+)/([A-Z ]+)(\\.[A-Z ]+)?\\^([0-9]{2})([0-9]{2})").exec(t);
if (!s) return null;
let r = s[1];
return e.luhnChecksum(r) ? {
type: "amex",
account: r,
lastName: s[2],
firstName: s[3],
honorific: s[4] ? s[4].trim().slice(1) : "",
expYear: s[5],
expMonth: s[6]
} : null;
}
},
// State definitions:
states: { IDLE: 0, PENDING1: 1, PENDING2: 2, READING: 3, DISCARD: 4, PREFIX: 5 },
// State names used when debugging.
stateNames: { 0: "IDLE", 1: "PENDING1", 2: "PENDING2", 3: "READING", 4: "DISCARD", 5: "PREFIX" },
// Holds current state. Update only through state function.
currentState: 0,
// Gets or sets the current state.
state: function() {
if (arguments.length === 0)
return e.currentState;
let t = arguments[0];
if (t != e.state) {
if (e.settings.debug && console.log("%s -> %s", e.stateNames[e.currentState], e.stateNames[t]), t == e.states.READING) {
let n = new CustomEvent("scanstart.cardswipe");
document.dispatchEvent(n);
}
if (e.currentState == e.states.READING) {
let n = new CustomEvent("scanend.cardswipe");
document.dispatchEvent(n);
}
e.currentState = t;
}
},
// Array holding scanned characters
scanbuffer: [],
// Interdigit timer
timerHandle: 0,
// Keypress listener
listener: function(t) {
switch (e.settings.debug && console.log(t.which + ": " + String.fromCharCode(t.which)), e.state()) {
// IDLE: Look for prfix characters or line 1 or line 2 start
// characters, and jump to PENDING1 or PENDING2.
case e.states.IDLE:
e.isInPrefixCodes(t.which) && (e.state(e.states.PREFIX), t.preventDefault(), t.stopPropagation(), e.startTimer()), t.which == 37 && (e.state(e.states.PENDING1), e.scanbuffer = [], e.processCode(t.which), t.preventDefault(), t.stopPropagation(), e.startTimer()), t.which == 59 && (e.state(e.states.PENDING2), e.scanbuffer = [], e.processCode(t.which), t.preventDefault(), t.stopPropagation(), e.startTimer());
break;
// PENDING1: Look for A-Z then jump to READING.
// Otherwise, pass the keypress through, reset and jump to IDLE.
case e.states.PENDING1:
if (t.which >= 65 && t.which <= 90 || t.which >= 97 && t.which <= 122) {
e.state(e.states.READING);
let n = document.querySelector(":focus");
n && n.blur(), e.processCode(t.which), t.preventDefault(), t.stopPropagation(), e.startTimer();
} else
e.clearTimer(), e.scanbuffer = null, e.state(e.states.IDLE);
break;
// PENDING_LINE2: look for 0-9, then jump to READING.
// Otherwise, pass the keypress through, reset and jump to IDLE.
case e.states.PENDING2:
if (t.which >= 48 && t.which <= 57) {
e.state(e.states.READING);
let n = document.querySelector(":focus");
n && n.blur(), e.processCode(t.which), t.preventDefault(), t.stopPropagation(), e.startTimer();
} else
e.clearTimer(), e.scanbuffer = null, e.state(e.states.IDLE);
break;
// READING: Copy characters to buffer until newline, then process the scanned characters
case e.states.READING:
e.processCode(t.which), e.startTimer(), t.preventDefault(), t.stopPropagation(), t.which == 13 && (e.clearTimer(), e.state(e.states.IDLE), e.processScan()), e.settings.firstLineOnly && t.which == 63 && (e.state(e.states.DISCARD), e.processScan());
break;
// DISCARD: Eat up characters until newline, then jump to IDLE
case e.states.DISCARD:
if (t.preventDefault(), t.stopPropagation(), t.which == 13) {
e.clearTimer(), e.state(e.states.IDLE);
return;
}
e.startTimer();
break;
// PREFIX: Eat up characters until % is seen, then jump to PENDING1
case e.states.PREFIX:
if (e.isInPrefixCodes(t.which)) {
e.state(states.IDLE);
return;
}
t.preventDefault(), t.stopPropagation(), t.which == 37 && (e.state(states.PENDING1), e.scanbuffer = [], e.processCode(t.which)), t.which == 59 && (e.state(states.PENDING2), e.scanbuffer = [], e.processCode(t.which)), e.startTimer();
}
},
// Converts a scancode to a character and appends it to the buffer.
processCode: function(t) {
e.scanbuffer.push(String.fromCharCode(t));
},
startTimer: function() {
clearTimeout(e.timerHandle), e.timerHandle = setTimeout(e.onTimeout, e.settings.interdigitTimeout);
},
clearTimer: function() {
clearTimeout(e.timerHandle), e.timerHandle = 0;
},
// Invoked when the timer lapses.
onTimeout: function() {
e.settings.debug && console.log("Timeout!"), e.state() == e.states.READING && e.processScan(), e.scanbuffer = null, e.state(states.IDLE);
},
// Processes the scanned card
processScan: function() {
e.settings.debug && console.log(e.scanbuffer);
let t = e.scanbuffer.join("");
e.settings.rawDataCallback && e.settings.rawDataCallback.call(this, t);
let n = e.parseData(t);
n ? (e.settings.success && e.settings.success.call(this, n, t), document.dispatchEvent(new CustomEvent("success.cardswipe", { detail: { rawData: t, result: n } }))) : (e.settings.failure && e.settings.failure.call(this, t), document.dispatchEvent(new CustomEvent("failure.cardswipe", { detail: { rawData: t } })));
},
// Invokes parsers until one succeeds, and returns the parsed result,
// or null if none succeed.
parseData: function(t) {
for (let n = 0; n < e.settings.parsers.length; n++) {
let s = e.settings.parsers[n], r;
if (typeof s == "function" ? r = s : typeof s == "string" && (r = e.builtinParsers[s]), r != null) {
let a = r.call(this, t);
if (a == null)
continue;
return a;
}
}
return null;
},
bindOn: function(t, n, s) {
n.split(".").reduce(function(r, a) {
return a = a ? a + "." + r : r, t.addEventListener(a, s, !0), a;
}, "");
},
bindOff: function(t, n, s) {
n.split(".").reduce(function(r, a) {
return a = a ? a + "." + r : r, t.removeEventListener(a, s, !0), a;
}, "");
},
// Binds the event listener
bindListener: function() {
document.addEventListener("keypress", e.listener);
},
// Unbinds the event listener
unbindListener: function() {
document.removeEventListener("keypress", e.listener);
},
// Default callback used if no other specified. Works with default parser.
defaultSuccessCallback: function(t) {
let n = ["Line 1: ", t.line1, `
Line 2: `, t.line2, `
Line 3: `, t.line3].join("");
alert(n);
},
isInPrefixCodes: function(t) {
return e.settings.prefixCodes ? e.settings.prefixCodes.indexOf(t) !== -1 : !1;
},
// Apply the Luhn checksum test. Returns true on a valid account number.
// The input is assumed to be a string containing only digits.
luhnChecksum: function(t) {
let n = [0, 2, 4, 6, 8, 1, 3, 5, 7, 9], s = 0, r = t.length, a = !0;
for (; r--; ) {
let i = parseInt(t.charAt(r), 10);
a ? s += i : s += n[i], a = !a;
}
return s % 10 === 0 && s > 0;
},
// Callable plugin methods
methods: {
init: function(t) {
let n = {
enabled: !0,
interdigitTimeout: 250,
success: e.defaultSuccessCallback,
failure: null,
parsers: ["visa", "mastercard", "amex", "discover", "generic"],
firstLineOnly: !1,
prefixCharacter: null,
debug: !1
};
if (e.settings = Object.assign(n, t), e.settings.prefixCharacter) {
Object.prototype.toString.call(e.settings.prefixCharacter) === "[object Array]" || (e.settings.prefixCharacter = [e.settings.prefixCharacter]), e.settings.prefixCodes = [];
for (let r of e.settings.prefixCharacter) {
if (r.length != 1)
throw "prefixCharacter must be a single character";
e.settings.prefixCodes.push(r.charCodeAt(0));
}
}
e.clearTimer(), e.state(e.states.IDLE), e.scanbuffer = null, e.unbindListener(), e.settings.enabled && e.methods.enable();
},
disable: function() {
e.unbindListener();
},
enable: function() {
e.bindListener();
}
}
};
export {
e as default
};