UNPKG

@danports/cardswipe

Version:

Capture & parse data from magnetic stripe card readers.

268 lines (267 loc) 10.7 kB
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 };