diff --git a/examples/card-form/index.html b/examples/card-form/index.html
index e4824ff6..9d38ccee 100644
--- a/examples/card-form/index.html
+++ b/examples/card-form/index.html
@@ -51,11 +51,12 @@
color: "#ababab",
},
},
+ exposeIIN8: true,
},
function (form) {
let preferredCardType = null;
form.getNumberField().on("input", function (e) {
- if (e.card_number_length == 6) {
+ if (e.card_number_length == 8) {
client.getCardInformation(
e.card_iin,
function(cardInfo) {
@@ -65,7 +66,7 @@
const radioGroup = document.createElement('div')
radioGroup.className = 'combo-card-types'
radioGroup.style.cssText = 'margin: 15px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; background: #f9f9f9;'
-
+
radioGroup.innerHTML = `
Select Card Type:
${cardInfo.combo_card_types.map((type, index) => `
@@ -75,11 +76,11 @@ Select Card Type:
`).join('')}
`
-
+
// Insert before the Pay button
const payButton = document.querySelector('.submit-button')
payButton.parentNode.insertBefore(radioGroup, payButton)
-
+
// Add event listener to track selection changes
const radioButtons = radioGroup.querySelectorAll('input[name="cardType"]')
radioButtons.forEach(radio => {
@@ -88,7 +89,7 @@ Select Card Type:
console.log("User selected card type:", preferredCardType)
})
})
-
+
// Set initial value
preferredCardType = null
}
@@ -98,7 +99,7 @@ Select Card Type:
}
)
}
-
+
document.getElementById("errors").innerHTML = ""
})
diff --git a/package.json b/package.json
index 4b6112af..faa35f1b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "processout.js",
- "version": "1.9.10",
+ "version": "1.9.11",
"description": "ProcessOut.js is a JavaScript library for ProcessOut's payment processing API.",
"scripts": {
"build:processout": "tsc -p src/processout && uglifyjs --compress --keep-fnames --ie8 dist/processout.js -o dist/processout.js",
diff --git a/src/dynamic-checkout/payment-methods/card.ts b/src/dynamic-checkout/payment-methods/card.ts
index 7dfb18cc..8c3faa35 100644
--- a/src/dynamic-checkout/payment-methods/card.ts
+++ b/src/dynamic-checkout/payment-methods/card.ts
@@ -933,7 +933,13 @@ module ProcessOut {
return
}
- const isAllowedIin = restrictToIins.indexOf(iin) !== -1
+ // card_iin may carry 6 or 8 digits (8 when the merchant opted into
+ // exposeIIN8 and PCI rules allow), while configured entries can be 6 or
+ // 8 digits, so match on prefix: an allowed entry matches when the
+ // detected IIN starts with it.
+ const isAllowedIin = restrictToIins.some(function (allowedIin) {
+ return allowedIin.length > 0 && iin.substring(0, allowedIin.length) === allowedIin
+ })
this.setCardRestrictionState(!isAllowedIin)
}
diff --git a/src/processout/card.ts b/src/processout/card.ts
index 480d8789..795be0c3 100644
--- a/src/processout/card.ts
+++ b/src/processout/card.ts
@@ -461,6 +461,63 @@ module ProcessOut {
return number.substring(0, l);
}
+ /**
+ * GetIIN8 returns the IIN of the card number, exposing up to 8 digits
+ * when PCI rules allow it (scheme in the allow-list and a 16-digit
+ * PAN) and 6 digits otherwise.
+ * @param {string} number
+ * @return {string}
+ */
+ public static getIIN8(number: string): string {
+ number = Card.parseNumber(number); // Remove potential spaces
+
+ if (number.length < 6)
+ return number;
+
+ if (Card.canExpose8DigitIIN(number))
+ return number.substring(0, 8);
+
+ return number.substring(0, 6);
+ }
+
+ /**
+ * Schemes permitted to surface an 8-digit IIN, mirroring the backend
+ * allow-list in api (controllers/card_inn.go). Every other scheme -
+ * notably American Express - is capped at 6 digits. Note the JS
+ * scheme key "union-pay" maps to the backend's "china union pay".
+ */
+ private static iin8DigitSchemes: Array = [
+ "visa", "mastercard", "discover", "jcb", "union-pay", "carte bancaire"
+ ];
+
+ /**
+ * canExpose8DigitIIN reports whether an 8-digit IIN may be surfaced
+ * for the given card number. Mirrors binder's TruncateNumber: the PAN
+ * must be exactly 16 digits and every detected scheme must be in the
+ * allow-list. Conservative on co-badged or ambiguous prefixes (and
+ * unknown schemes), which fall back to 6 digits.
+ * @param {string} number
+ * @return {boolean}
+ */
+ public static canExpose8DigitIIN(number: string): boolean {
+ number = Card.parseNumber(number); // Remove potential spaces
+
+ // PCI: 8-digit BINs are only defined for 16-digit PANs.
+ if (number.length != 16)
+ return false;
+
+ var schemes = Card.getPossibleSchemes(number);
+ if (schemes.length == 0)
+ return false;
+
+ for (var i = 0; i < schemes.length; i++) {
+ if (Card.iin8DigitSchemes.indexOf(schemes[i]) === -1)
+ return false;
+ }
+
+ return true;
+ }
+
/**
* GetLast4Digits returns the last4 digits of the card number
* @param {string} number
diff --git a/src/processout/cardfield.ts b/src/processout/cardfield.ts
index 7fe9b4c2..f315852a 100644
--- a/src/processout/cardfield.ts
+++ b/src/processout/cardfield.ts
@@ -24,6 +24,7 @@ module ProcessOut {
public cardNumberAutoNext: boolean = true;
public enableCardSchemeSelection: boolean = false;
public preferredSchemes: string[] = null;
+ public exposeIIN8: boolean = false;
public constructor(type: string) {
this.type = type;
@@ -33,10 +34,11 @@ module ProcessOut {
if (o.placeholder) this.placeholder = o.placeholder;
if (o.style) this.style = o.style;
if (o.requireCVC != null) this.requireCVC = o.requireCVC;
- if (o.expiryAutoNext !== undefined && o.expiryAutoNext !== null) this.expiryAutoNext = o.expiryAutoNext;
- if (o.cardNumberAutoNext !== undefined && o.cardNumberAutoNext !== null) this.cardNumberAutoNext = o.cardNumberAutoNext;
- if (o.enableCardSchemeSelection !== undefined && o.enableCardSchemeSelection !== null) this.enableCardSchemeSelection = o.enableCardSchemeSelection;
- if (o.preferredSchemes !== undefined && o.preferredSchemes !== null) this.preferredSchemes = o.preferredSchemes;
+ if (o.expiryAutoNext !== undefined && o.expiryAutoNext !== null) this.expiryAutoNext = o.expiryAutoNext;
+ if (o.cardNumberAutoNext !== undefined && o.cardNumberAutoNext !== null) this.cardNumberAutoNext = o.cardNumberAutoNext;
+ if (o.enableCardSchemeSelection !== undefined && o.enableCardSchemeSelection !== null) this.enableCardSchemeSelection = o.enableCardSchemeSelection;
+ if (o.preferredSchemes !== undefined && o.preferredSchemes !== null) this.preferredSchemes = o.preferredSchemes;
+ if (o.exposeIIN8 !== undefined && o.exposeIIN8 !== null) this.exposeIIN8 = o.exposeIIN8;
return this;
}
@@ -77,55 +79,55 @@ module ProcessOut {
* Card field class
*/
export class CardField {
- /**
+ /**
* Number is the credit card field type number
* @var {string}
*/
public static number = "number";
- /**
+ /**
* Expiry is the credit card field type expiration date
* @var {string}
*/
public static expiry = "expiry";
- /**
+ /**
* ExpiryMonth is the credit card field type expiration month
* @var {string}
*/
public static expiryMonth = "expiry-month";
- /**
+ /**
* ExpiryYear is the credit card field type expiration year
* @var {string}
*/
public static expiryYear = "expiry-year";
- /**
+ /**
* CVC is the credit card field type cvc
* @var {string}
*/
public static cvc = "cvc";
- /**
+ /**
* Timeout is the number of ms to wait before timing out a field
* @var {string}
*/
protected static timeout = 20000;
- /**
+ /**
* instance is the current ProcessOut instance
* @var {ProcessOut}
*/
protected instance: ProcessOut;
- /**
+ /**
* El is the parent of the iframe used to embed the field
* @var {string}
*/
protected el: HTMLElement;
- /**
+ /**
* Iframe is the iframe embedding the field
* @var {string}
*/
@@ -143,7 +145,7 @@ module ProcessOut {
*/
protected form: CardForm;
- /**
+ /**
* Callback executed when an event is triggered on an input
* @var {Callback}
*/
@@ -191,9 +193,9 @@ module ProcessOut {
* @param {options} CardFieldOptions
* @param {HTMLElement} el
*/
- public constructor(instance: ProcessOut, form: CardForm,
- options: CardFieldOptions, container: HTMLElement,
- success: () => void,
+ public constructor(instance: ProcessOut, form: CardForm,
+ options: CardFieldOptions, container: HTMLElement,
+ success: () => void,
error: (err: Exception) => void) {
if (!options || !options.type) {
@@ -256,7 +258,7 @@ module ProcessOut {
* @return {void}
*/
protected spawn(
- success: () => void,
+ success: () => void,
error: (err: Exception) => void
): void {
var tmp = Math.random().toString(36).substring(7);
@@ -278,7 +280,7 @@ module ProcessOut {
if (typeof(error) !== typeof(Function)) {
error = function () {}
}
-
+
var errored = false;
var iframeError = setTimeout(function() {
errored = true;
@@ -289,12 +291,12 @@ module ProcessOut {
try {
// We want to reset the iframe src to prevent
// Firefox from (wrongfully) caching the iframe
- // content: https://bugzilla.mozilla.org/show_bug.cgi?id=354176
+ // content: https://bugzilla.mozilla.org/show_bug.cgi?id=354176
if(navigator.userAgent.match(/firefox|fxios/i)) {
if (this.iframe && this.iframe.contentWindow) {
this.iframe.contentWindow.location.replace(endpoint);
}
- }
+ }
} catch(e) { /* ... */ }
}.bind(this);
@@ -319,7 +321,7 @@ module ProcessOut {
// and the iframe should reply with a ready state
if (data.action == "alive") {
-
+
// The field's iframe is available, let's set it up
this.postMessage(JSON.stringify({
"namespace": Message.fieldNamespace,
@@ -387,7 +389,7 @@ module ProcessOut {
for (const mutation of mutations) {
for (const removedNode of Array.from(mutation.removedNodes)) {
// Check if our iframe was removed directly or as part of a parent
- if (removedNode === this.iframe ||
+ if (removedNode === this.iframe ||
(removedNode instanceof Element && removedNode.contains(this.iframe))) {
this.destroy();
return;
@@ -395,7 +397,7 @@ module ProcessOut {
}
}
});
-
+
// Observe the document body for child removals (subtree to catch parent removals)
this.mutationObserver.observe(document.body, {
childList: true,
@@ -465,7 +467,7 @@ module ProcessOut {
if (this.eventCallback) this.eventCallback("onfocus", d);
break;
case "blurEvent": // inverse of focus
- // Remove the processout-input-focused class from the
+ // Remove the processout-input-focused class from the
// parent element
this.el.className = this.el.className
.replace(/\bprocessout-input-focused\b/g, "")
@@ -490,8 +492,8 @@ module ProcessOut {
case "resize":
if (this.options.style?.height) {
this.iframe.height = this.options.style.height;
- } else {
- this.iframe.height = data.data;
+ } else {
+ this.iframe.height = data.data;
}
break;
}
@@ -512,7 +514,7 @@ module ProcessOut {
* @return {void}
*/
public update(options: CardFieldOptions): void {
- if (options.placeholder)
+ if (options.placeholder)
this.options.placeholder = options.placeholder;
if (options.style)
this.options.style = (Object).assign(
@@ -538,10 +540,10 @@ module ProcessOut {
}
/**
- * addEventListener adds an event listener for the given event on
+ * addEventListener adds an event listener for the given event on
* the card field
- * @param {string} e
- * @param {callback} h
+ * @param {string} e
+ * @param {callback} h
* @return {void}
*/
public addEventListener(e: string, h: (e: any) => void): void {
@@ -570,8 +572,8 @@ module ProcessOut {
/**
* on adds an event listener for the given event on the card field
- * @param {string} e
- * @param {callback} h
+ * @param {string} e
+ * @param {callback} h
* @return {void}
*/
public on(e: string, h: (e: any) => void): void {
@@ -665,7 +667,7 @@ module ProcessOut {
setTimeout(function(){
error(new Exception("processout-js.field.unavailable"));
}, CardField.timeout);
-
+
window.addEventListener("message", function (event) {
var data = Message.parseEvent(event);
if (data.frameID != this.uid)
@@ -688,10 +690,10 @@ module ProcessOut {
}.bind(this));
}
- /**
+ /**
* Tokenize asks the leader field to tokenize using the sub-fields
* and calls success with the final card token
- * @param {any[]} fields
+ * @param {any[]} fields
* @param {callback} success
* @param {callback} error
* @return {void}
@@ -702,7 +704,7 @@ module ProcessOut {
if (typeof(error) !== typeof(Function)) {
error = () => {};
}
-
+
// Tell our field it should start the tokenization process and
// expect a response
var id = Math.random().toString();
@@ -752,11 +754,11 @@ module ProcessOut {
}.bind(this));
}
- /**
- * refreshCVC asks the field to refresh the CVC of the given card.
+ /**
+ * refreshCVC asks the field to refresh the CVC of the given card.
* The success callback is called with the card UID if it was successful
* otherwise the error callback is called with the Exception
- * @param {any[]} fields
+ * @param {any[]} fields
* @param {callback} success
* @param {callback} error
* @return {void}
diff --git a/src/processout/processout.ts b/src/processout/processout.ts
index 2ae80956..d5a8df4c 100644
--- a/src/processout/processout.ts
+++ b/src/processout/processout.ts
@@ -1676,7 +1676,10 @@ module ProcessOut {
return
}
- const iin = cardNumber.substring(0, 6)
+ // Support up to 8-digit IINs (some networks issue 8-digit IINs, which
+ // yield more accurate issuer information); fall back to whatever is
+ // available when fewer digits were provided.
+ const iin = cardNumber.substring(0, 8)
const apiEndpoint = `iins/${iin}`
this.apiRequest(