UNPKG

@securecall/client-component

Version:

SecureCall Core Web Component

1,465 lines (1,444 loc) 71.6 kB
import { r as registerInstance, c as createEvent, h, H as Host } from './index-CHf4eHvJ.js'; import { L as Logger, C as CLIENT_COMP_VERSION } from './logger-CgjdSc_8.js'; /** * Performs a deep merge of objects and returns new object. Does not modify * objects (immutable) and merges arrays via concatenation. * * @param {...object} objects - Objects to merge * @returns {object} New object with merged key/values */ function mergeDeep(...objects) { const isObject = (obj) => obj && typeof obj === 'object'; return objects.reduce((prev, obj) => { Object.keys(obj).forEach(key => { const pVal = prev[key]; const oVal = obj[key]; if (oVal === undefined) { return; // Skip undefined values } if (Array.isArray(pVal) && Array.isArray(oVal)) { prev[key] = pVal.concat(oVal); } else if (isObject(pVal) && isObject(oVal)) { prev[key] = mergeDeep(pVal, oVal); } else { prev[key] = oVal; } }); return prev; }, {}); } class RequestField { order; value; readOnly; hidden; valid; possibleValues; mapping; externalMapping; label; component; min; max; active; placeholder; hideRelatedFields; secure; optional; _name; _owner; constructor(owner, name) { this._name = name; this._owner = owner; return new Proxy(this, { set(target, prop, value, receiver) { const propStr = prop.toString(); // Check if it's a public property (doesn't start with underscore) if (!propStr.startsWith('_') && propStr in target) { // Call propagate on the owner if it exists if (target._owner && typeof target._owner.propagate === 'function') { target._owner.propagate(target._name, propStr, value); } } return Reflect.set(target, prop, value, receiver); } }); } toJSON() { return { [this._name]: this.calculatedValue() }; } calculatedValue() { const result = {}; for (const key in this) { if (this.hasOwnProperty(key) // find properties && !key.startsWith('_') // remove private ones by naming convention && this[key] !== undefined) { // ignore undefined ones result[key] = this[key]; } } return result; } } class MetadataField { _name; _value; constructor() { this._name = 'metadata'; this._value = {}; return new Proxy(this, { get(target, prop, receiver) { const propStr = prop.toString(); const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(target)).concat(Object.getOwnPropertyNames(target)); // console.log('** metadata get:', propStr, methods) if (methods.includes(propStr)) { return Reflect.get(target, prop, receiver); } else { return target.rawValue[propStr]; } }, set(target, prop, v, receiver) { const propStr = prop.toString(); const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(target)).concat(Object.getOwnPropertyNames(target)); // console.log('** metadata set:', propStr, methods) if (methods.includes(propStr)) { return Reflect.set(target, prop, v, receiver); } else { target.rawValue[propStr] = v; return true; } } }); } toJSON() { return { [this._name]: this.calculatedValue() }; } calculatedValue() { return { value: this._value }; } get rawValue() { return this._value; } } class RequestFieldList { _fields; _uiFields; _level; constructor(uiFields, fieldLevel = FieldLevelType.Instance) { this._uiFields = uiFields; this._level = fieldLevel; this._fields = { metadata: new MetadataField() }; return new Proxy(this, { get(target, prop, receiver) { const propStr = prop.toString(); if (target.parent.fieldNames.includes(propStr)) { if (!target.rawFields[propStr]) { target.rawFields[propStr] = new RequestField(target, propStr); } return target.rawFields[propStr]; } return Reflect.get(target, prop, receiver); }, }); } get parent() { return this._uiFields; } get rawFields() { return this._fields; } addNewField(name, config) { if (!this.parent.fieldNames.includes(name)) { this.parent.fieldNames.push(name); Object.entries(config).forEach(([k, v]) => { this[name][k] = v; }); } else { throw new Error(`Field ${name} already exists in the request fields list`); } } propagate(fieldName, prop, value) { if (this.parent.fieldNames.includes(fieldName)) { if (this._level === FieldLevelType.Instance) { this.propagateValue(this.parent.session, fieldName, prop, value); } else if (this._level === FieldLevelType.Session) { this.propagateValue(this.parent.transaction, fieldName, prop, value); } } } propagateValue(fieldList, fieldName, prop, value) { if (fieldList.rawFields[fieldName]) { fieldList[fieldName][prop] = value; } } calculatedFields() { return Object.fromEntries(Object.entries(this._fields) .filter(([key]) => this.parent.fieldNames.includes(key)) .map(([fieldName, field]) => [ fieldName, field.calculatedValue() ])); } } var FieldLevelType; (function (FieldLevelType) { FieldLevelType["Instance"] = "instance"; FieldLevelType["Session"] = "session"; FieldLevelType["Transaction"] = "transaction"; })(FieldLevelType || (FieldLevelType = {})); class RequestFieldGroup { _defaults; _instance; _session; _transaction; _fieldNames; constructor(defaults) { this._defaults = JSON.parse(JSON.stringify(defaults)); this._fieldNames = Object.keys(defaults); this._instance = new RequestFieldList(this, FieldLevelType.Instance); this._session = new RequestFieldList(this, FieldLevelType.Session); this._transaction = new RequestFieldList(this, FieldLevelType.Transaction); return new Proxy(this, { get(target, prop, receiver) { const propStr = prop.toString(); if (target.fieldNames.includes(propStr)) { return target.instance[propStr]; } return Reflect.get(target, prop, receiver); }, }); } get fieldNames() { return this._fieldNames; } get instance() { return this._instance; } get session() { return this._session; } get transaction() { return this._transaction; } addNewField(name, config) { this._instance.addNewField(name, config); } processSecureData(data) { const calc = this.calculatedFields(); //Reset the active flag for each secure this._fieldNames.forEach(k => { if (calc[k].secure) this._transaction[k].active = undefined; }); Object.entries(data).forEach(([k, v]) => { let f = this._transaction[k]; Object.entries(v).forEach(([k2, v2]) => { f[k2] = v2; }); }); } clearNonSecureFields() { const calc = this.calculatedFields(); this._fieldNames.forEach(k => { if (!calc[k].secure) this._transaction[k].value = undefined; }); } resetInstance() { this._instance = new RequestFieldList(this, FieldLevelType.Instance); } resetSession() { this._session = new RequestFieldList(this, FieldLevelType.Session); } resetTransaction() { this._transaction = new RequestFieldList(this, FieldLevelType.Transaction); } calculatedFields() { return mergeDeep(this._defaults, this._instance.calculatedFields(), this._session.calculatedFields(), this._transaction.calculatedFields()); } asComponentChildren() { return Object.entries(this.calculatedFields()) .filter(([_k, v]) => !v.hidden) .sort(([_ak, av], [_bk, bv]) => { const orderA = av.order === undefined ? 1000 : av.order; const orderB = bv.order === undefined ? 1000 : bv.order; return orderA - orderB; }) .map(([k, v]) => { const { component, ...config } = v; return { name: k, component: component || 'string', config }; }); } asAPITransaction() { let fields = { metadata: {} }; Object.entries(this.calculatedFields()) .filter(([_k, v]) => !v.secure) .filter(([_k, v]) => !(v.optional && (!v.value || v.value.trim() === ''))) .forEach(([k, v]) => { if (v.mapping) { let mapping = v.mapping.split('.'); if (mapping[0] === 'metadata') { fields.metadata[mapping[1]] = v.value; } else { fields[mapping[0]] = v.value; } } else { if (k === 'metadata') { fields[k] = { ...fields.metadata, ...v.value }; } else { fields[k] = v.value; } } }); return fields; } } const DEFAULT_REQUEST_FIELDS = { transactionType: { order: 10, mapping: 'transaction_type', label: 'Transaction Type', placeholder: 'Select Transaction Type', component: 'radio', valid: true, value: 'pay', readOnly: false, hidden: false, possibleValues: { pay: 'Pay', pay_and_tokenise: 'Pay and Tokenise', tokenise: 'Tokenise' }, hideRelatedFields: { pay: { amount: false, paymentReference: false, tokenReference: true }, pay_and_tokenise: { amount: false, paymentReference: false, tokenReference: false }, tokenise: { amount: true, paymentReference: true, tokenReference: false } } }, amount: { order: 20, label: 'Amount', placeholder: 'Enter amount', component: 'currency', valid: false, readOnly: false, hidden: false, min: 0.01, max: 999999.99 }, paymentReference: { order: 30, mapping: 'payment_reference', label: 'Payment Ref', placeholder: 'Payment Reference', component: 'string', valid: false, readOnly: false, hidden: false }, tokenReference: { order: 40, mapping: 'token_reference', label: 'Customer Ref', placeholder: 'Customer ID or Reference', component: 'string', valid: false, readOnly: false, hidden: false }, nameOnCard: { order: 50, mapping: 'name_on_card', label: 'Name On Card', component: 'string', valid: false, readOnly: false, hidden: false }, gatewayName: { order: 60, mapping: 'metadata.gateway_name', label: 'Gateway', component: 'select', valid: false, readOnly: false, hidden: true, possibleValues: { default: 'Default Gateway' } }, pan: { order: 100, label: 'Card Number', component: 'secure-pan', valid: false, secure: true, readOnly: false, hidden: false }, expiry: { order: 110, label: 'Expiry Date', component: 'secure', valid: false, secure: true, readOnly: false, hidden: false }, cvv: { order: 120, label: 'CVV', component: 'secure', secure: true, valid: false, readOnly: false, hidden: false }, metadata: { order: 1000, label: 'Metadata', component: 'metadata', valid: true, readOnly: false, hidden: true }, }; var CURRENT_STATE_T; (function (CURRENT_STATE_T) { CURRENT_STATE_T["NotAuthenticated"] = "NotAuthenticated"; CURRENT_STATE_T["Idle"] = "Idle"; CURRENT_STATE_T["Securing"] = "Securing"; CURRENT_STATE_T["Secured"] = "Secured"; CURRENT_STATE_T["Submitting"] = "Submitting"; CURRENT_STATE_T["Success"] = "Success"; CURRENT_STATE_T["Failure"] = "Failure"; })(CURRENT_STATE_T || (CURRENT_STATE_T = {})); var __defProp = Object.defineProperty; var __defProps = Object.defineProperties; var __getOwnPropDescs = Object.getOwnPropertyDescriptors; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __hasOwnProp = Object.prototype.hasOwnProperty; var __propIsEnum = Object.prototype.propertyIsEnumerable; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]); } return a; }; var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); var __objRest = (source, exclude) => { var target = {}; for (var prop in source) if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0) target[prop] = source[prop]; if (source != null && __getOwnPropSymbols) for (var prop of __getOwnPropSymbols(source)) { if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop)) target[prop] = source[prop]; } return target; }; var __async = (__this, __arguments, generator) => { return new Promise((resolve, reject) => { var fulfilled = (value) => { try { step(generator.next(value)); } catch (e) { reject(e); } }; var rejected = (value) => { try { step(generator.throw(value)); } catch (e) { reject(e); } }; var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected); step((generator = generator.apply(__this, __arguments)).next()); }); }; // src/parse.ts function getBytes(stream, onChunk) { return __async(this, null, function* () { const reader = stream.getReader(); let result; while (!(result = yield reader.read()).done) { onChunk(result.value); } reader.releaseLock(); }); } function getLines(onLine, onTimeout, timeoutMs = 3e4) { let buffer; let position; let fieldLength; let discardTrailingNewline = false; let timeoutId; let keepAliveReceived = false; const resetTimeout = () => { if (timeoutId) { clearTimeout(timeoutId); } if (onTimeout && !keepAliveReceived) { timeoutId = setTimeout(() => { onTimeout(); }, timeoutMs); } }; const clearTimeoutIfExists = () => { if (timeoutId) { clearTimeout(timeoutId); timeoutId = void 0; } }; resetTimeout(); return function onChunk(arr) { if (buffer === void 0) { buffer = arr; position = 0; fieldLength = -1; keepAliveReceived = false; resetTimeout(); } else { buffer = concat(buffer, arr); } const bufLength = buffer.length; let lineStart = 0; while (position < bufLength) { if (discardTrailingNewline) { if (buffer[position] === 10 /* NewLine */) { lineStart = ++position; } discardTrailingNewline = false; } let lineEnd = -1; for (; position < bufLength && lineEnd === -1; ++position) { switch (buffer[position]) { case 58 /* Colon */: if (fieldLength === -1) { fieldLength = position - lineStart; keepAliveReceived = true; clearTimeoutIfExists(); } break; // @ts-ignore:7029 \r case below should fallthrough to \n: case 13 /* CarriageReturn */: discardTrailingNewline = true; case 10 /* NewLine */: lineEnd = position; break; } } if (lineEnd === -1) { break; } onLine(buffer.subarray(lineStart, lineEnd), fieldLength); lineStart = position; fieldLength = -1; keepAliveReceived = false; resetTimeout(); } if (lineStart === bufLength) { buffer = void 0; } else if (lineStart !== 0) { buffer = buffer.subarray(lineStart); position -= lineStart; } }; } function getMessages(onId, onRetry, onMessage) { let message = newMessage(); const decoder = new TextDecoder(); return function onLine(line, fieldLength) { if (line.length === 0) { onMessage == null ? void 0 : onMessage(message); message = newMessage(); } else if (fieldLength > 0) { const field = decoder.decode(line.subarray(0, fieldLength)); const valueOffset = fieldLength + (line[fieldLength + 1] === 32 /* Space */ ? 2 : 1); const value = decoder.decode(line.subarray(valueOffset)); switch (field) { case "data": message.data = message.data ? message.data + "\n" + value : value; break; case "event": message.event = value; break; case "id": onId(message.id = value); break; case "retry": const retry = parseInt(value, 10); if (!isNaN(retry)) { onRetry(message.retry = retry); } break; } } }; } function concat(a, b) { const res = new Uint8Array(a.length + b.length); res.set(a); res.set(b, a.length); return res; } function newMessage() { return { data: "", event: "", id: "", retry: void 0 }; } // src/errorClasses.ts var RetriableError = class extends Error { constructor(message) { super(message); this.name = "RetriableError"; } }; var HTTPError = class extends Error { constructor(code, message) { super(message); this.name = "HTTPError"; this.code = code; } }; var HTTPClientError = class extends HTTPError { constructor(code, message) { super(code, message); this.name = "HTTPClientError"; } }; var FailedRetriesError = class extends Error { constructor(message) { super(message); this.name = "FailedRetriesError"; } }; var NotConnectedError = class extends Error { constructor(message) { super(message); this.name = "NotConnectedError"; } }; // src/fetch.ts var EventStreamContentType = "text/event-stream"; var DefaultRetryInterval = 1e3; var LastEventId = "last-event-id"; var HeartbeatTimer = 0; function fetchEventSource(input, _a) { var _b = _a, { signal: inputSignal, headers: inputHeaders, onopen: inputOnOpen, onmessage, onclose, onerror, fetch: inputFetch, heartbeat: inputHeartbeat } = _b, rest = __objRest(_b, [ "signal", "headers", "onopen", "onmessage", "onclose", "onerror", "fetch", "heartbeat" ]); return new Promise((resolve, reject) => { const headers = __spreadValues({}, inputHeaders); if (!headers.accept) { headers.accept = EventStreamContentType; } let curRequestController; let retryInterval = DefaultRetryInterval; let retryTimer = 0; function dispose() { window.clearTimeout(retryTimer); curRequestController.abort(); } inputSignal == null ? void 0 : inputSignal.addEventListener("abort", () => { dispose(); resolve(); }); const heartbeat = inputHeartbeat != null ? inputHeartbeat : HeartbeatTimer; const fetch2 = inputFetch != null ? inputFetch : window.fetch; const onopen = inputOnOpen != null ? inputOnOpen : defaultOnOpen; function create() { return __async(this, null, function* () { var _a2, _b2; curRequestController = new AbortController(); try { const response = yield fetch2(input, __spreadProps(__spreadValues({}, rest), { headers, signal: curRequestController.signal })); yield onopen(response); yield getBytes(response.body, getLines( getMessages((id) => { if (id) { headers[LastEventId] = id; } else { delete headers[LastEventId]; } }, (retry) => { retryInterval = retry; }, onmessage), heartbeat > 0 ? () => { curRequestController.abort("timeout"); } : void 0, heartbeat )); onclose == null ? void 0 : onclose(); dispose(); resolve(); } catch (err) { if (!curRequestController.signal.aborted) { try { const interval = (_a2 = onerror == null ? void 0 : onerror(err)) != null ? _a2 : retryInterval; window.clearTimeout(retryTimer); retryTimer = window.setTimeout(create, interval); } catch (innerErr) { dispose(); reject(innerErr); } } else if (curRequestController.signal.reason === "timeout") { try { const timeoutError = new RetriableError(`Timeout: No traffic received within ${heartbeat}ms`); const interval = (_b2 = onerror == null ? void 0 : onerror(timeoutError)) != null ? _b2 : retryInterval; window.clearTimeout(retryTimer); retryTimer = window.setTimeout(create, interval); } catch (innerErr) { dispose(); reject(innerErr); } } } }); } create(); }); } function defaultOnOpen(response) { const contentType = response.headers.get("content-type"); if (!(contentType == null ? void 0 : contentType.startsWith(EventStreamContentType))) { throw new Error(`Expected content-type to be ${EventStreamContentType}, Actual: ${contentType}`); } } // src/constants.ts var LOGIN_PATH = "/v1/login"; var SECURE_PATH = "/v1/secure"; var RELEASE_PATH = "/v1/release"; var EVENTS_PATH = "/v1/events"; var RESET_PATH = "/v1/reset"; var SUBMIT_PATH = "/v1/submit"; var LOGOUT_PATH = "/v1/logout"; var TELEPHONY_PATH = "/v1/telephony"; var DEFAULT_HEADERS = { "Content-Type": "application/json" }; var MAX_RETRIES = 2; var HEARTBEAT_TIMEOUT = 45e3; var HTTP_STATUS_TEXT = { 400: "Bad Request", 403: "Forbidden", 404: "Not Found", 408: "Request Timeout", 500: "Internal Server Error", 501: "Not Implemented", 502: "Bad Gateway", 503: "Service Unavailable" }; // src/events.ts var AuthenticateEvent = class extends Event { constructor(success, reason, code) { super("authentication"); this.success = success; this.reason = reason; this.code = code; if (reason === void 0 && this.code !== void 0) { this.reason = code == null ? void 0 : code.toString(); } } }; var StreamEvent = class _StreamEvent extends Event { static createEvent(type, data) { let evt; switch (type) { case "configuration": evt = new ConfigurationEvent(data); break; case "status": evt = new SessionEvent(data); break; case "telephony": evt = new TelephonyEvent(data); break; default: evt = new _StreamEvent(type, data); break; } return evt; } constructor(type, data) { super(type); this.detail = data; } }; var ConfigurationEvent = class extends StreamEvent { constructor(data) { super("configuration", data); } get settings() { return this.detail.settings; } get gateways() { return this.detail.gateways; } }; var SessionEvent = class extends StreamEvent { constructor(data) { super("session", data); } get call_data() { return this.detail.call_data; } get secure_data() { return this.detail.secure_data; } get transaction_data() { return this.detail.transaction_data; } get state() { return this.detail.state; } }; var TelephonyEvent = class extends StreamEvent { constructor(data) { super("telephony", data); } get session() { return this.detail.session; } get command() { return this.detail.command; } get dnis() { return this.detail.dnis; } }; // src/version.ts var CLIENT_LIB_JS_VERSION = "1.0.0"; // src/index.ts var SecureCallClientAPI = class extends EventTarget { /** * * @param apiLocation - The URL prefix for all communication to the SecureCall Client API * @param callerId - added to all logging if passed */ constructor(apiLocation, callerId) { super(); this.useCookie = false; this.connected = false; this.instanceId = Math.floor(Math.random() * 1e8); this.popupWindow = null; this.webexFlag = false; this.retryCount = 0; this.logOutput = console; this.isDebugActive = false; this.apiLocation = apiLocation; this.logPrefix = `SecureCallClientAPI(${CLIENT_LIB_JS_VERSION}/${this.instanceId})`; if (callerId) { this.logPrefix = `${this.logPrefix}->${callerId}`; } this.debug = true; } /** * @returns true if the event stream connection is established */ get isConnected() { return this.connected; } /** * * @param isDebug - set to true to have log output go to the logger or false to stop logging */ set debug(isDebug) { this.isDebugActive = isDebug; if (isDebug) { const timestamp = function() { }; timestamp.toString = function() { return (/* @__PURE__ */ new Date()).toISOString(); }; this.log = this.logOutput.debug.bind(this.logOutput, timestamp.toString(), this.logPrefix); } else { this.log = function() { }; } } /** * @returns true if debug logging is active */ get debug() { return this.isDebugActive; } /** * * @param l - any object which exposes a function called 'debug' equivalent to console.debug */ set logger(l) { this.logOutput = l; this.debug = this.isDebugActive; } /** * * @param email - email address of the user * @param password - password of the user. This could be a Webex Org ID, SecureCall API Key or a password * @param useCookie - true if cookies will be used for all API requests or false to send the email/password each time */ authenticate(email, password, useCookie) { return __async(this, null, function* () { this.log("authenticate"); this.email = email != null ? email : this.email; this.password = password != null ? password : this.password; if (typeof useCookie != "undefined") { this.useCookie = useCookie; } return this.connect(); }); } /** * login uses the email/password to log into the Client API. Typically only used when cookies are used. */ login() { return __async(this, null, function* () { let response; try { this.log("login"); response = yield fetch(this.apiLocation + LOGIN_PATH, { method: "POST", credentials: "include", headers: this.clientAPIHeaders() }); this.log("login", "response", response); if (response.redirected) { return this.handleRedirect(response); } else if (response.ok) { return this.connectEventStream(); } } catch (error) { this.log("login", "error", error.message); response = new Response(null, { status: 400, statusText: error.message }); } this.asyncDispatchEvent(new AuthenticateEvent(false, response.statusText, response.status)); this.throwHTTPError(response.status, response.statusText); }); } /** * Logs out of the SecureCall Client API and disconnects the event stream */ logout() { return __async(this, null, function* () { this.log("logout"); if (!this.connected) { throw new NotConnectedError(); } this.disconnect(); let response; try { response = yield fetch(this.apiLocation + LOGOUT_PATH, { method: "POST", credentials: "include", headers: this.clientAPIHeaders() }); this.log("logout", "response", response); if (response.redirected) { return this.handleRedirect(response); } else if (response.ok) { return; } } catch (error) { this.log("logout", "error", error.message); response = new Response(null, { status: 400, statusText: error.message }); } this.throwHTTPError(response.status, response.statusText); }); } /** * Attempts to secure the call. All updates come through the event stream and will be emitted as Events */ secure(body) { return __async(this, null, function* () { this.log("secure"); if (!this.connected) { throw new NotConnectedError(); } let response; try { response = yield fetch(this.apiLocation + SECURE_PATH, { method: "POST", credentials: "include", headers: this.clientAPIHeaders(), body: JSON.stringify(body != null ? body : {}) }); this.log("secure", "response", "", response); if (response.redirected) { this.handleRedirect(response); return; } else if (response.ok) { return; } else if (response.status === 401 || response.status === 403) { this.asyncDispatchEvent(new AuthenticateEvent(false, response.statusText, response.status)); } } catch (error) { this.log("secure", "error", error.message); response = new Response(null, { status: 400, statusText: error.message }); } this.throwHTTPError(response.status, response.statusText); }); } /** * Attempts to release the call. All updates come through the event stream and will be emitted as Events */ release() { return __async(this, null, function* () { this.log("release"); if (!this.connected) { throw new NotConnectedError(); } let response; try { response = yield fetch(this.apiLocation + RELEASE_PATH, { method: "POST", credentials: "include", headers: this.clientAPIHeaders() }); this.log("release", "response", "", response); if (response.redirected) { this.handleRedirect(response); return; } else if (response.ok) { return; } else if (response.status === 401 || response.status === 403) { this.asyncDispatchEvent(new AuthenticateEvent(false, response.statusText, response.status)); } } catch (error) { this.log("release", "error", error.message); response = new Response(null, { status: 400, statusText: error.message }); } this.throwHTTPError(response.status, response.statusText); }); } /** * * @param v - if empty, will attempt to reset all the PCI fields, if a string will attempt to reset just that field or can be an array of field name */ reset(v) { return __async(this, null, function* () { let data; this.log("reset"); if (!this.connected) { throw new NotConnectedError(); } if (v === void 0) { data = ["all"]; } else if (typeof v === "string") { data = [v]; } else { data = v; } let response; try { response = yield fetch(this.apiLocation + RESET_PATH, { method: "POST", credentials: "include", headers: this.clientAPIHeaders(), body: JSON.stringify({ fields: data }) }); this.log("reset", "response", response); if (response.redirected) { return this.handleRedirect(response); } else if (response.ok) { return; } else if (response.status === 401 || response.status === 403) { this.asyncDispatchEvent(new AuthenticateEvent(false, response.statusText, response.status)); } } catch (error) { this.log("reset", "error", error.message); response = new Response(null, { status: 400, statusText: error.message }); } this.throwHTTPError(response.status, response.statusText); }); } /** * * @param data - submits payment data */ submit(data) { return __async(this, null, function* () { this.log("submit"); if (!this.connected) { throw new NotConnectedError(); } if (data === void 0) { throw new Error("no parameter"); } let response; try { this.log("submit", data); response = yield fetch(this.apiLocation + SUBMIT_PATH, { method: "POST", credentials: "include", headers: this.clientAPIHeaders(), body: JSON.stringify(data) }); this.log("submit", "response", "", response); if (response.redirected) { return this.handleRedirect(response); } else if (response.ok) { return; } else if (response.status === 401 || response.status === 403) { this.asyncDispatchEvent(new AuthenticateEvent(false, response.statusText, response.status)); } } catch (error) { this.log("submit", "error", error.message); response = new Response(null, { status: 400, statusText: error.message }); } this.throwHTTPError(response.status, response.statusText); }); } /** * * @param data - update telephony state for Client Telephony */ telephony(data) { return __async(this, null, function* () { this.log("telephony"); if (!this.connected) { throw new NotConnectedError(); } if (data === void 0) { throw new Error("no parameter"); } let response; try { response = yield fetch(this.apiLocation + TELEPHONY_PATH, { method: "POST", credentials: "include", headers: this.clientAPIHeaders(), body: JSON.stringify(data) }); this.log("telephony", "response", "", response); if (response.redirected) { return this.handleRedirect(response); } else if (response.ok) { return; } else if (response.status === 401 || response.status === 403) { this.asyncDispatchEvent(new AuthenticateEvent(false, response.statusText, response.status)); } } catch (error) { this.log("telephony", "error", error.message); response = new Response(null, { status: 400, statusText: error.message }); } this.throwHTTPError(response.status, response.statusText); }); } /** * connects the event stream using email/password/useCookie */ connect() { if (!this.useCookie && !this.email && !this.password) { throw new Error("email and password need to be set for useCookie to be false"); } if (this.email && !this.password) { throw new Error("password needs to be set"); } if (this.useCookie) { return this.login(); } else { return this.connectEventStream(); } } /** * disconnects the event stream connection */ disconnect() { var _a; this.log("disconnect"); (_a = this.abortController) == null ? void 0 : _a.abort(); } handleAPIResponse(response) { this.log("handleAPIResponse"); if (response.redirected) { this.handleRedirect(response); } else if (response.ok) { this.log("handleAPIResponse", "doLogin was successful", response); return this.connectEventStream(); } else { this.throwHTTPError(response.status, response.statusText); } } handleRedirect(response) { this.log("handleRedirect", response.url); this.popupWindow = window.open(response.url, "popup", "width=600,height=600"); if (this.popupWindow) { let timer = setInterval(() => { var _a; if ((_a = this.popupWindow) == null ? void 0 : _a.closed) { clearInterval(timer); this.log("handleRedirect", "detected popup window was closed"); this.asyncDispatchEvent(new AuthenticateEvent(false, "popup window closed")); } }, 500); window.addEventListener("message", (event) => { if (event.data === "webexLoginSuccess") { clearInterval(timer); this.log("handleRedirect", "window eventListener", "Webex login was successful"); this.connectEventStream(); } }, { once: true }); } } connectEventStream() { return __async(this, null, function* () { var _a; this.log("connectEventStream"); (_a = this.abortController) == null ? void 0 : _a.abort(); this.abortController = new AbortController(); const signal = this.abortController.signal; yield fetchEventSource(this.apiLocation + EVENTS_PATH, { method: "GET", credentials: this.shouldSendCredentials() ? "same-origin" : "include", headers: this.eventSourceHeaders(), signal, onopen: this.onEventStreamOpen.bind(this), onmessage: this.onEventStreamMessage.bind(this), onclose: this.onEventStreamClose.bind(this), onerror: this.onEventStreamError.bind(this), heartbeat: HEARTBEAT_TIMEOUT }); }); } onEventStreamOpen(response) { return __async(this, null, function* () { var _a; this.log("onEventStreamOpen", "status=", response.status); if (response.redirected) { this.log("onEventStreamOpen", "response.redirected", response); this.webexFlag = true; return this.handleRedirect(response); } else if (response.ok) { if ((_a = response.headers.get("content-type")) == null ? void 0 : _a.startsWith(EventStreamContentType)) { this.dispatchEvent(new AuthenticateEvent(true)); this.connected = true; this.log("onEventStreamOpen", "text/event-stream response.ok", response); } else { this.log("onEventStreamOpen", "text/html response.ok", response); throw new Error("Received an unexpected text/html page"); } } else if (response.status >= 400 && response.status <= 499) throw new HTTPClientError(response.status, response.statusText); else throw new RetriableError("onopen retriable"); }); } onEventStreamMessage(msg) { this.log("onEventStreamMessage", "event", msg.event, msg); this.asyncDispatchEvent(StreamEvent.createEvent(msg.event, JSON.parse(msg.data))); } onEventStreamClose() { this.connected = false; if (this.webexFlag) { this.log("onEventStreamClose", "doing_popup so ignore"); this.webexFlag = false; } else { this.log("onEventStreamClose"); throw new RetriableError("close"); } } onEventStreamError(error) { this.log("onEventStreamError", error); if (error instanceof RetriableError) { if (this.retryCount < MAX_RETRIES) { const delay = Math.pow(2, this.retryCount) * 1e3; this.retryCount++; this.log(`Retrying in ${delay} milliseconds...`); return delay; } else { this.asyncDispatchEvent(new AuthenticateEvent(false, "no more retries")); this.retryCount = 0; throw new FailedRetriesError(); } } else { this.asyncDispatchEvent(new AuthenticateEvent(false, error.message, error == null ? void 0 : error.code)); this.retryCount = 0; throw error; } } asyncDispatchEvent(evt) { return __async(this, null, function* () { this.dispatchEvent(evt); }); } shouldSendCredentials() { return this.email && this.password; } authenticationHeader() { if (this.email && this.password) { return { Authorization: "Basic " + btoa(this.email + ":" + this.password) }; } else { return {}; } } eventSourceHeaders() { return __spreadValues(__spreadProps(__spreadValues({}, this.authenticationHeader()), { Accept: "text/event-stream, text/html" }), this.logHeaders()); } clientAPIHeaders() { return __spreadValues(__spreadValues(__spreadValues({}, DEFAULT_HEADERS), this.authenticationHeader()), this.logHeaders()); } logHeaders() { return { "X-SecureCall-Client-Identifier": this.logPrefix }; } throwHTTPError(status, text) { if (!text) { text = this.lookupHTTPText(status); } if (status >= 400 && status <= 499) throw new HTTPClientError(status, text); else throw new HTTPError(status, text); } // HTTP/2 doesn't return statusText in the response so we need to do a lookup lookupHTTPText(status) { return HTTP_STATUS_TEXT[status] || "Unknown"; } }; class ResponseFields { _defaults; _result; _fieldNames; constructor(defaults) { this._defaults = JSON.parse(JSON.stringify(defaults)); this._fieldNames = Object.keys(defaults); this._result = {}; } get defaults() { return this._defaults; } toJSON() { return this.calculatedFields(); } processResponse(data) { if (data === undefined || data === null) { return; } this._fieldNames.forEach(k => { const mapping = this._defaults[k].mapping || `response.${k}`; let maps = mapping.split('.'); const val = maps.reduce((obj, key) => obj?.[key], data); if (val !== undefined) { this._result[k] = { value: val }; } }); } get fieldNames() { return this._fieldNames; } asComponentChildren(success) { return Object.entries(this.calculatedFields()) .filter(([_k, v]) => { return (success) ? v.showOnSuccess : v.showOnFailure; }) .filter(([_k, v]) => { return (!(v.ignoreIfEmpty && (v.value === null || v.value === undefined || v.value === ''))); }) .sort(([_ak, av], [_bk, bv]) => { const orderA = av.order === undefined ? 1000 : av.order; const orderB = bv.order === undefined ? 1000 : bv.order; return orderA - orderB; }) .map(([k, v]) => { return { name: k, label: v.label, value: v.value, action: v.action, component: v.component || 'field', }; }); } calculatedFields() { return mergeDeep(this._defaults, this._result); } } const DEFAULT_RESPONSE_FIELDS = { transactionId: { order: 10, label: 'Transaction ID', mapping: 'response.transaction_id', showOnSuccess: true }, receipt: { order: 20, label: 'Receipt', showOnSuccess: true }, token: { order: 30, label: 'Token', showOnSuccess: false }, errorMessage: { order: 100, label: 'Error Message', mapping: 'response.error_message', showOnFailure: false }, errorCode: { order: 110, label: 'Error Code', mapping: 'response.error_code', showOnFailure: false }, anotherTransaction: { order: 200, label: 'Another Transaction', action: 'retryClearCVV', showOnSuccess: true, showOnFailure: true, component: 'button' } }; const PENDING_CALL_SECURE_TIMEOUT = 35000; const PENDING_PAYMENT_TIMEOUT = 60000; const AUTHENTICATION_RETRIES = 3; const securecallClientCss = ":host{display:flex;flex-direction:column;align-items:center;width:fit-content;min-width:300px;margin:auto;--theme-background-color-dark:none;--theme-primary-color-dark:white;--theme-secondary-color-dark:#ccc;--theme-button-color-dark:rgba(4, 156, 196, 0.6);--theme-button-color-hover-dark:rgba(4, 156, 196, 1);--theme-button-color-disabled-dark:rgba(4, 156, 196, 0.3);--theme-text-disabled-color-dark:rgba(4, 156, 196, 1);--theme-border-color-dark:#ccc;--theme-border-color-disabled-dark:rgba(4, 156, 196, 0.3);--theme-input-active-background-dark:white;--theme-input-inactive-background-dark:#ccc;--theme-spinner-border-dark:rgba(255, 255, 255, 0.3);--theme-spinner-color-dark:rgba(4, 156, 196, 1);--theme-background-color-light:none;--theme-primary-color-light:#333;--theme-secondary-color-light:#ccc;--theme-button-color-light:rgba(4, 156, 196, 0.6);--theme-button-color-hover-light:rgba(4, 156, 196, 1);--theme-button-color-disabled-light:rgba(4, 156, 196, 0.3);--theme-text-disabled-color-light:rgba(4, 156, 196, 0.6);--theme-border-color-light:#ccc;--theme-border-color-disabled-light:rgba(4, 156, 196, 0.3);--theme-input-active-background-light:white;--theme-input-inactive-background-light:#ccc;--theme-spinner-border-light:rgba(0, 0, 0, 0.3);--theme-spinner-color-light:rgba(4, 156, 196, 1)}.theme-dark{width:100%;background-color:var(--theme-background-color-dark);--theme-primary-color:var(--theme-primary-color-dark);--theme-secondary-color:var(--theme-secondary-color-dark);--theme-button-color:var(--theme-button-color-dark);--theme-button-color-hover:var(--theme-button-color-hover-dark);--theme-button-color-disabled:var(--theme-button-color-disabled-dark);--theme-text-disabled-color:var(--theme-text-disabled-color-dark);--theme-border-color:var(--theme-border-color-dark);--theme-border-color-disabled:var(--theme-border-color-disabled-dark);--theme-input-active-background:var(--theme-input-active-background-dark);--theme-input-inactive-background:var(--theme-input-inactive-background-dark);--theme-spinner-border:var(--theme-spinner-border-dark);--theme-spinner-color:var(--theme-spinner-color-dark)}.theme-light{width:100%;background-color:var(--theme-background-color-light);--theme-primary-color:var(--theme-primary-color-light);--theme-secondary-color:var(--theme-secondary-color-light);--theme-button-color:var(--theme-button-color-light);--theme-button-color-hover:var(--theme-button-color-hover-light);--theme-button-color-disabled:var(--theme-button-color-disabled-dark);--theme-text-disabled-color:var(--theme-text-disabled-color-light);--theme-border-color:var(--theme-border-color-light);--theme-border-color-disabled:var(--theme-border-color-disabled-light);--theme-input-active-background:var(--theme-input-active-background-light);--theme-input-inactive-background:var(--theme-input-inactive-background-light);--theme-spinner-border:var(--theme-spinner-border-light);--theme-spinner-color:var(--theme-spinner-color-light)}.component-button{width:fit-content;padding:10px 20px;font-size:16px;color:var(--theme-primary-color);background-color:var(--theme-button-color);border:1px solid var(--theme-secondary-color);border-radius:5px;transition:background-color 0.3s ease, border-color 0.3s ease;cursor:pointer}.component-button:hover{background-color:var(--theme-button-color-hover);border-color:var(--theme-button-color-hover)}.component-button:disabled{background-color:var(--theme-button-color-disabled);border-color:var(--theme-border-color-disabled);color:var(--theme-text-disabled-color);cursor:not-allowed;pointer-events:none}.secure-form{text-align:center}.response-result-container{text-align:center;font-size:1.2rem;font-weight:bold}.loading-spinner{display:flex;justify-content:center;align-items:center;height:var(--spinner-container-size, 50px)}.spinner{position:relative;width:var(--spinner-container-size, 50px);height:var(--spinner-container-size, 50px)}.spinner-circle{border:4px solid var(--theme-spinner-border);border-top:4px solid var(--theme-spinner-color);border-radius:50%;width:var(--spinner-circle-size, 20px);height:var(--spinner-circle-size, 20px);animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}"; const SecurecallClient = class { constructor(hostRef) { registerInstance(this, hostRef); this.authenticationSuccess = createEvent(this, "authenticationSuccess"); this.authenticationFailure = createEvent(this, "authenticationFailure"); this.configurationChanged = createEvent(this, "configurationChanged"); this.callSecured = createEvent(this, "callSecured"); this.callEnded = createEvent(this, "callEnded"); this.transactionSubmitted = createEvent(this, "transactionSubmitted"); this.transactionSubmitting = createEvent(this, "transactionSubmitting"); this.transactionSuccess = createEvent(this, "transactionSuccess"); this.transactionFailure = createEvent(this, "transactionFailure"); this.loggedOut = createEvent(this, "loggedOut"); this.telephonyHold = createEvent(this, "telephonyHold"); this.telephonyRecover = createEvent(this, "telephonyRecover"); this.telephonyLegA = createEvent(this, "telephonyLegA"); this.telephonyLegB = createEvent(this, "telephonyLegB");