Yes, I did manage to reverse-engineer the way Epicor Web Access calls the service. No documentation, the source code I decompiled is half broken (from factory!), and there’s a bunch of useless obfuscation to get through, but it does work. Here’s basically how you do it, with two small JS scripts:
// Handler for messages from IFrame
function handleTokenMessage (e) {
var rsp = e.data;
if (rsp.operation == 'token' && rsp.params.success) {
console.log(rsp.params);
// Got the token! From here we need to save it to DB.
var token = rsp.params.token;
var exp = rsp.params.expdate;
var cardType = rsp.params.cardtype;
var cvv = rsp.params.cvv;
var response = 'STATUS=0000:COMPLETE,TOKEN=' + token + ',EXPDATE=' + exp + ',CARDTYPE=' + cardType + ',CVV=' + cvv;
}
$('#divLoading').hide();
};
function openHostedTokenPage() {
$('#divLoading').show();
// Listen to message from child IFrame...
window.removeEventListener('message', handleTokenMessage, false);
window.addEventListener('message', handleTokenMessage, false);
const host = 'https://sce.toogo.io';
const xlmns = 'http://epicor.com/webservices/';
const scriptUrl = '/hp/hostedpayments.js';
const publicKey = 'pk_0Zq6VpALF3Q7Fp5tW8LyiTZzasAIq2d6'; // Epicor EPX public key (lifted from debugging an EWA token entry)
const siteID = '00000000'; // Epicor customer Site ID
// Get and run the server script that will add the IFrame to the DOM and show the credit card entry modal.
$.getScript(host + scriptUrl, function() {
var config = [ { xmlns: xlmns },
{ public_key: publicKey },
{ ccnamespace: siteID },
{ name: "@Html.Raw(@Resources.Global.ccTitle)" },
{ button: "@Html.Raw(Resources.Global.ccSubmit)" },
{ translation_config: [ { xmlns: xlmns },
{ ccnum_lbl: "@Html.Raw(Resources.Global.ccCardNumber)" },
...
{ ccnum_err: "@Html.Raw(Resources.Global.ccCardNumberErr)" } ] },
{ style_config: [ { xmlns: xlmns },
{ control_font: "inherit" },
{ control_font_color: "Black" },
...
{ btn_font_background: "White" } ] } ];
// No idea why it's doing all that below, seems like simple (and useless) obfuscation to me, but
// clearly not very effective... I just left it as I found it, seems to work fine, but slow...
for (var l = config, ccnuPlh = "", c = "", u = "", p = "", h = "", d = "", g = "", m = "", b = "", S = "", y = "", f = "", C = "", w = "", v = "", E = "", T = "", D = "", k = "", A = "", I = "", G = "", _ = 0; _ < l.length; _++)
void 0 != l[_].public_key && (c = l[_].public_key),
void 0 != l[_].name && (u = l[_].name),
void 0 != l[_].button && (p = l[_].button),
void 0 != l[_].translation_config && (h = l[_].translation_config),
void 0 != l[_].style_config && (d = l[_].style_config);
for (var _ = 0; _ < h.length; _++)
void 0 != h[_].ccnum_lbl && (g = h[_].ccnum_lbl),
void 0 != h[_].ccnum_plh && (ccnuPlh = h[_].ccnum_plh),
void 0 != h[_].expiry_lbl && (m = h[_].expiry_lbl),
void 0 != h[_].expiry_plh && (b = h[_].expiry_plh),
void 0 != h[_].cvv_lbl && (S = h[_].cvv_lbl),
void 0 != h[_].cvv_phl && (y = h[_].cvv_phl),
void 0 != h[_].expiry_err1 && (f = h[_].expiry_err1),
void 0 != h[_].ccnum_err && (C = h[_].ccnum_err),
void 0 != h[_].cvv_err && (w = h[_].cvv_err);
for (var _ = 0; _ < d.length; _++)
void 0 != d[_].control_font && (v = d[_].control_font),
void 0 != d[_].control_font_size && (E = d[_].control_font_size),
void 0 != d[_].control_font_color && (T = d[_].control_font_color),
void 0 != d[_].control_background && (D = d[_].control_background),
void 0 != d[_].btn_font && (k = d[_].btn_font),
void 0 != d[_].btn_font_size && (A = d[_].btn_font_size),
void 0 != d[_].btn_font_color && (I = d[_].btn_font_color),
void 0 != d[_].btn_font_background && (G = d[_].btn_font_background);
HostedPayment.setup({
response: function (e) {
// In case there's an error inserting the IFrame in the DOM...
if (!e.success) alert(e.error);
},
service_uri: host
}).open({
// Show the modal in the IFrame.
public_key: c,
namespace: siteID,
name: u,
button: p,
style_config: {
control_font: v,
control_font_size: E,
control_font_color: T,
control_background: D,
btn_font: k,
btn_font_size: A,
btn_font_color: I,
btn_font_background: G
},
translation_config: {
ccnum_lbl: g,
ccnum_plh: ccnuPlh,
expiry_lbl: m,
expiry_plh: b,
cvv_lbl: S,
cvv_phl: y,
expiry_err1: f,
ccnum_err: C,
cvv_err: "" == w ? w : "Invalid CVV/CVC."
}
});
});
}