var crypto = window.crypto || window.msCrypto;
var PayComponent = (function ()
{
const eResult_success = 0;
const eResult_failed = -1;
const eResult_access_denied = -3;
const TITLE = "HyperPay";
const PAY_SIZE = { width: 477, height: 750 };
const PACK_NAME = `billing-service`;
const PACK_VERS = `1.0.123.0`;
const CLIENT_ID = `5fd16945-38fa-4e88-9d74-a71677c6dac7`;
const DEBUG = true;
const ORIGIN_URL = `https://app.hyperpay.cloud`;
const MESSAGE_LOADED = 'pay_inited';
const ID_IMG_LOADING = 'img-popup-loading';
const MESSAGE_RESULT = 'pay_result';
const RES_SUCCESS = 'success';
const RES_EXISTS = 'exists';
const RES_CANCEL = 'cancel';
const RES_FAILED = 'failed';
PayComponent.prototype.SESSION_ID = `aa966028-aedd-4bc9-ac6d-5d330d5e926b`;
PayComponent.prototype.LOCALE = ``;
PayComponent.prototype.THEME = `dark`;
function PayComponent()
{
console.log(`${PACK_NAME}: v.${PACK_VERS}`);
this.resultData = {};
this.request_invoices = {};
this.current_request = undefined;
this.documentLoadedListener = this.onDocumentLoad.bind(this);
this.BeforeUnloadListener = this.onBeforeUnload.bind(this);
this.MessageListener = this.onMessage.bind(this);
this.PayStartListener = this.onPayStart.bind(this);
}
function nowToTimeStr() {
const nowDate = new Date(Date.now());
const formatOptions = {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
};
return nowDate.toLocaleTimeString(undefined, formatOptions);
}
function log_msg(sender, text) {
return `${nowToTimeStr()} [${sender.constructor.name}] ${text}`;
}
function log_err(sender, error) {
const text = error['message'] ?? `${error}`;
return `${nowToTimeStr()} [${sender.constructor.name}] ${text}`;
}
function is_result_cancel(result) {
if (result === RES_CANCEL)
return true;
return false;
}
function is_result_success(result) {
if (result === RES_SUCCESS ||
result === RES_EXISTS)
return true;
return false;
}
PayComponent.prototype.check_message = function (event, message) {
const _this = this;
if (event.origin == ORIGIN_URL &&
event.data['session_id'] == _this.SESSION_ID &&
event.data['message'] == message)
return true;
return false;
}
function arrayBufferToBase64(buffer) {
var binary = '';
var bytes = new Uint8Array(buffer);
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
function base64ToArrayBuffer(base64) {
var binary_string = window.atob(base64);
var len = binary_string.length;
var bytes = new Uint8Array(len);
for (var i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
return bytes.buffer;
}
function importRsaKey(binaryDer) {
return crypto.subtle.importKey(
"spki",
binaryDer,
{
name: "RSA-OAEP",
hash: "SHA-256"
},
false,
["encrypt"]
);
}
function messageEncode(message) {
let enc = new TextEncoder();
return enc.encode(message);
}
function encryptMessage(publicKey, encoded) {
return crypto.subtle.encrypt(
{
name: "RSA-OAEP",
},
publicKey,
encoded
);
}
async function sendRequest(method, url, authorization) {
const options = {
method
}
if (authorization) {
options.headers = {
"Authorization": "Bearer " + authorization
}
}
const responce = await fetch(url, options);
let result;
try {
result = await responce.json();
}
catch (err) { }
if (responce.status != 200 || eResult_success != result['result']) {
return {
result: result['result'] || eResult_failed,
message: result?.message || `failed code: ${responce.status}`
};
}
return result;
}
PayComponent.prototype.onMessage = function (event)
{
let _this = this;
if (_this.check_message(event, MESSAGE_LOADED)) {
if (DEBUG) console.debug(log_msg(_this, `= on popup loaded`));
const imgEl = document.getElementById(ID_IMG_LOADING);
imgEl.className = "is-hidden";
}
if (_this.check_message(event, MESSAGE_RESULT)) {
if (DEBUG) console.debug(log_msg(_this, `= onMessage(${JSON.stringify(event.data)})`));
this.resultData = event.data;
}
}
PayComponent.prototype.onBeforeUnload = function () {
let _this = this;
if (DEBUG) console.debug(log_msg(_this, `= before unload`));
window.removeEventListener('beforeunload', _this.BeforeUnloadListener);
if (_this.payTab && !_this.payTab.closed)
{
_this.payTab.close();
}
}
PayComponent.prototype.popupWindow = function (url, title, width, height)
{
const left = (screen.width - width) / 2;
let top = (screen.height - height) / 2;
if (top > 100) top -= 30;
const options = `toolbar=no,location=no,directories=no,status=no,menubar=no,` +
`scrollbars=no,resizable=0,copyhistory=no,` +
`width=${width},height=${height},top=${top},left=${left}`;
return window.open(url, title, options);
}
PayComponent.prototype.closePayment = function () {
let _this = this;
if (_this.payTimer) {
clearInterval(_this.payTimer);
delete _this.payTimer;
}
window.removeEventListener('message', _this.MessageListener);
if (DEBUG) console.debug(log_msg(_this, `< on closed: "${_this.resultData['type']}"`));
if (_this.blockedPopup) {
document.body.removeChild(_this.blockedPopup);
delete _this.blockedPopup;
}
if (_this.payTab) {
delete _this.payTab;
}
const requestId = _this.current_request;
_this.current_request = undefined;
let invoiceId;
if (_this.request_invoices[requestId]) {
invoiceId = _this.request_invoices[requestId];
delete _this.request_invoices[requestId];
}
const data = {
result: _this.resultData['type'] || _this.resultData['result']
};
if (_this.resultData.reason) data['reason'] = _this.resultData.reason;
if (invoiceId) data['invoice_id'] = invoiceId;
if (_this.resultData.payment_id) data['payment_id'] = _this.resultData.payment_id;
if (_this.resultData.status) data['status'] = _this.resultData.status;
if (is_result_success(_this.resultData['type']) && _this.OPTIONS.onSuccess) {
_this.OPTIONS.onSuccess(requestId, data);
}
else if (is_result_cancel(_this.resultData['type']) && _this.OPTIONS.onCancel) {
_this.OPTIONS.onCancel(requestId, data);
}
else if (_this.OPTIONS.onError) {
_this.OPTIONS.onError(requestId, data);
}
}
PayComponent.prototype.addBlockedPopup = function ()
{
let _this = this;
const theme = _this.OPTIONS?.theme ?? 'dark';
const theme_bkColor = theme == 'dark' ? '0, 0, 0, 0.8' : '200, 200, 200, 0.8';
const theme_color = theme == 'dark' ? 'white' : 'black';
_this.blockedPopup = document.body.appendChild(document.createElement('div'));
_this.blockedPopup.style = `position: fixed;` +
`top: 0; left: 0;` +
`width: 100%; height: 100%;` +
`width: 100vw; height: 100vh;` +
`max-width: 100%; max-height: 100%;` +
`min-width: 100%; min-height: 100%;` +
`z-index: 2147483647;` +
`background-color: rgba(${theme_bkColor});` +
`color: ${theme_color}; cursor: pointer;`;
_this.blockedPopup.addEventListener('click', function () {
if (DEBUG) console.debug(log_msg(_this, `= switch to popup`));
if (_this.payTab) {
_this.payTab.focus();
}
});
const closePopupEl = _this.blockedPopup.appendChild(document.createElement('a'));
closePopupEl.textContent = "❌";
closePopupEl.href = '#';
closePopupEl.classList.add('popup-close');
closePopupEl.addEventListener('click', function (event) {
event.cancelBubble = true;
if (DEBUG) console.debug(log_msg(_this, `= force popup closed`));
if (_this.payTab) {
_this.payTab.close();
}
else {
_this.closePayment();
}
});
const popupInfoEl = _this.blockedPopup.appendChild(document.createElement('div'));
popupInfoEl.style = `position: absolute;` +
`top: 50%; left: 50%;` +
`transform: translateX(-50%) translateY(-50%);` +
`text-align: center;` +
`box-sizing: border-box;`;
popupInfoEl.innerHTML = `${TITLE}: to continue,
` +
`please complete cyber-pay.
` +
`
`;
}
PayComponent.prototype.onDocumentLoad = function () {
let _this = this;
if (DEBUG) console.debug(log_msg(_this, `= loaded`));
_this.isLoaded = true;
document.removeEventListener('DOMContentLoaded', _this.documentLoadedListener);
_this.continueInitAsync();
}
PayComponent.prototype.continueInitAsync = async function ()
{
let _this = this;
// if (DEBUG) await new Promise(resolve => setTimeout(resolve, 3000)); // sleep for 3 sec
// check options
if (!_this.OPTIONS.onPreparePay && !_this.OPTIONS.onPreparePayAsync) { // _this.OPTIONS.onPrepareInvoice
_this.INIT_STATUS = 'failed';
_this.onInited(false, `Any of functions 'onPreparePay' or 'onPreparePayAsync' is required`);
return;
}
if (!_this.OPTIONS.onClientAssertion && !_this.OPTIONS.onClientAssertionAsync) {
_this.INIT_STATUS = 'failed';
_this.onInited(false, `Any of functions 'onClientAssertion' or 'onClientAssertionAsync' is required`);
return;
}
// load assertion params
let response;
if (_this.SESSION_ID) {
const assertionUrl = `${ORIGIN_URL}/api/js/get-assertion-params?session_id=${_this.SESSION_ID}`;
response = await sendRequest('POST', assertionUrl);
}
if (!response || response.result == eResult_access_denied)
{
const assertionUrl = `${ORIGIN_URL}/api/js/get-assertion-params?client_id=${CLIENT_ID}`;
response = await sendRequest('POST', assertionUrl);
}
if (response.result != eResult_success) {
_this.INIT_STATUS = 'failed';
_this.onInited(false, response.message || `get issuer failed`);
return;
}
// create assertion
if (_this.OPTIONS.onClientAssertion) {
_this.ASSERTION = _this.OPTIONS.onClientAssertion(response.assertion_params);
}
else if (_this.OPTIONS.onClientAssertionAsync) {
_this.ASSERTION = await _this.OPTIONS.onClientAssertionAsync(response.assertion_params);
}
// init: check assertion and get client public_key for data encryption
const initUrl = `${ORIGIN_URL}/api/js/init`;
const initResponse = await sendRequest('POST', initUrl, _this.ASSERTION);
if (initResponse.result != eResult_success) {
_this.INIT_STATUS = 'failed';
_this.onInited(false, initResponse.message || `init failed`);
return;
}
_this.SESSION_ID = initResponse.session_id;
_this.ENCRYPTION_KEY = initResponse.public_key;
if (DEBUG) console.debug(log_msg(_this, `< end initialization: ENCRYPTION_KEY[${_this.ENCRYPTION_KEY.length}]`));
_this.INIT_STATUS = 'success';
_this.onInited(true);
}
PayComponent.prototype.loadPayData = async function()
{
let _this = this;
const requestUrl = `${ORIGIN_URL}/api/session/${_this.SESSION_ID}/new-request`;
const requestResponse = await sendRequest('POST', requestUrl, _this.ASSERTION);
if (requestResponse.result != eResult_success) {
throw new Error(requestResponse.message);
}
if (!_this.blockedPopup) {
throw new Error('request cancelled')
}
const request_id = requestResponse.request;
_this.current_request = request_id;
let payData;
if (_this.OPTIONS.onPreparePay)
payData = _this.OPTIONS.onPreparePay(request_id);
else if (_this.OPTIONS.onPreparePayAsync)
payData = await _this.OPTIONS.onPreparePayAsync(request_id);
if (payData['invoice_id']) {
_this.request_invoices[request_id] = payData['invoice_id'];
}
if (!payData) return;
payData['request_id'] = request_id;
const dataStr = JSON.stringify(payData);
const encodedArr = messageEncode(dataStr);
const encyptedB64 = arrayBufferToBase64(encodedArr);
return encyptedB64;
}
PayComponent.prototype.onPayStart = async function()
{
let _this = this;
if (DEBUG) console.debug(log_msg(_this, `> on click`));
try {
_this.resultData = { type: RES_CANCEL };
_this.addBlockedPopup();
const themeStr = _this.THEME ? `theme=${_this.THEME}&` : '';
const localeStr = _this.LOCALE ? `locale=${_this.LOCALE}&` : '';
const payData = await _this.loadPayData();
if (!payData) return;
const dataStr = `data=${payData}`;
const url = `${ORIGIN_URL}/pay/session/${_this.SESSION_ID}?${themeStr}${localeStr}${dataStr}`;
const title = TITLE; // "_blank";
window.addEventListener('message', _this.MessageListener);
window.addEventListener('beforeunload', _this.BeforeUnloadListener);
_this.payTab = _this.popupWindow(url, title, PAY_SIZE.width, PAY_SIZE.height);
if (!_this.payTab) {
throw new Error(`Can't open HyperPay pop-up. Please check browser settings and allow pop-ups.`);
}
_this.payTimer = setInterval(function () {
if (!_this.payTab || _this.payTab.closed) {
_this.closePayment();
}
}, 500);
}
catch (err) {
_this.resultData['type'] = RES_FAILED;
_this.resultData['reason'] = err.message;
_this.closePayment();
if (DEBUG) console.error(log_err(_this, err));
}
}
//#region Public Methods
PayComponent.prototype.init = function (options, onInited)
{
let _this = this;
if (DEBUG) console.debug(log_msg(_this, `> start initialization`));
_this.INIT_STATUS = 'start';
_this.OPTIONS = options;
if (_this.OPTIONS?.theme)
_this.THEME = _this.OPTIONS.theme;
if (_this.OPTIONS?.locale)
_this.LOCALE = _this.OPTIONS.locale;
_this.onInited = onInited;
if (_this.isLoaded) {
_this.continueInitAsync();
}
else {
if (DEBUG) console.debug(log_msg(_this, `= loading`));
document.addEventListener('DOMContentLoaded', _this.documentLoadedListener);
}
}
PayComponent.prototype.render = function (containerID)
{
let _this = this;
if (DEBUG) console.debug(log_msg(_this, `= render`));
if (!_this.btnEl) {
// init cp_animation styles dynamic
const css = `
.popup-close {
position: absolute;
right: 16px;
top: 16px;
width: 16px;
height: 16px;
font-size: initial;
padding-right: 6px;
padding-bottom: 6px;
text-decoration: none;
}
.is-hidden {
display: none;
}
.rotate {
animation: rotation 8s infinite linear;
}
@keyframes rotation {
from {
transform: rotate(0deg);
}
to {
transform: rotate(359deg);
}
}`;
const head = document.head || document.getElementsByTagName('head')[0];
const style = document.createElement('style');
head.appendChild(style);
style.type = 'text/css';
if (style.styleSheet) {
// This is required for IE8 and below.
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
// render button
const divEl = document.getElementById(containerID);
if (!divEl) {
throw new Error(`element ${containerID} in Document not found`);
}
if (divEl.tagName.toLowerCase() != 'div') {
throw new Error(`can not render hyperpay to ${divEl.tagName} element`);
}
if (_this.INIT_STATUS != 'success') {
throw new Error(`not inited`);
}
const theme = _this.THEME ?? 'dark';
const theme_bkColor = theme == 'dark' ? 'black' : 'lightgray';
const theme_brColor = theme == 'dark' ? 'black' : 'darkgray';
const theme_logo = theme == 'dark' ? '/Assets/images/logo-dark.svg' : '/Assets/images/logo-light.svg';
const containerEl = divEl.appendChild(document.createElement('div'));
containerEl.style = `position: relative;` +
`display: inline-block;` +
`width: 100%;` +
`min-height: 35px;` +
`min-width: 150px; max-width: 350px;` +
`font-size: 0;`;
_this.btnEl = containerEl.appendChild(document.createElement('button'));
_this.btnEl.style = `background-color: ${theme_bkColor};` +
`border-color: ${theme_brColor};` +
`width: 100%;` +
`border-radius: 4px;` +
`cursor: pointer;`;
const logoEl = _this.btnEl.appendChild(document.createElement('img'));
logoEl.src = `${ORIGIN_URL}${theme_logo}`;
logoEl.width = 100;
logoEl.style = `margin-top:4px;`;
_this.btnEl.addEventListener('click', _this.PayStartListener);
}
}
//#endregion
return PayComponent;
} ());
// export { PayComponent };