Source: paywall.src.js

// paywall-dev.js
// LIBRARY CLASS



(function(global){
    "use strict";


    /**
     * Enumeration of available states for a given payment flow.
     * @enum {string}
     * @readonly
     * @global
     */
    global.PaywallState = {
        /** Payment flow is new and no invoice have yet been generated. */
        NEW: "NEW",
        /** Invoice have been generated and is waiting to be settled. */
        INVOICE: "INVOICE",
        /**
         * Generated invoice have expired and a new payment flow have to be generated.
         */
        INVOICE_EXPIRED: "INVOICE_EXPIRED",
        /**
         * Payment have been settled and the payment flow should be ready to perform the call.
         * If multiple calls is possible is up to the settlement type.
         */
        SETTLED: "SETTLED",
        /**
         * Payment type is of type pay per request and request have been processed successfully.
         */
        EXECUTED: "EXECUTED",
        /**
         * Generated settlement is not yet valid and need to wait until call can be performed.
         */
        SETTLEMENT_NOT_YET_VALID: "SETTLEMENT_NOT_YET_VALID",
        /**
         * Generated settlement have expired and new payment flow have to be generated.
         */
        SETTLEMENT_EXPIRED: "SETTLEMENT_EXPIRED",
        /**
         * Paywall API related error occurred during processing of payment flow, see paywallError object for details.
         */
        PAYWALL_ERROR: "PAYWALL_ERROR",
        /**
         * Request was aborted by the user.
         */
        ABORTED : "ABORTED"
    };

    /**
     * Enumeration of available event types that might be triggered for a given payment flow.
     * The special ANY event is used when registering for event callback where all events should
     * be triggered.
     * @enum {string}
     * @readonly
     * @global
     */
    global.PaywallEventType = {
        /** Invoice have been generated and is waiting to be settled. */
        INVOICE: "INVOICE",
        /**
         * Generated invoice have expired and a new payment flow have to be generated.
         */
        INVOICE_EXPIRED: "INVOICE_EXPIRED",
        /**
         * Payment have been settled and the payment flow should be ready to perform the call.
         * If multiple calls is possible is up to the settlement type.
         */
        SETTLED: "SETTLED",
        /**
         * Payment type is of type pay per request and request have been processed successfully.
         */
        EXECUTED: "EXECUTED",
        /**
         * Generated settlement is not yet valid and need to wait until call can be performed.
         */
        SETTLEMENT_NOT_YET_VALID: "SETTLEMENT_NOT_YET_VALID",
        /**
         * Generated settlement have expired and new payment flow have to be generated.
         */
        SETTLEMENT_EXPIRED: "SETTLEMENT_EXPIRED",
        /**
         * Paywall API related error occurred during processing of payment flow, see paywallError object for details.
         */
        PAYWALL_ERROR: "PAYWALL_ERROR",
        /**
         * Special value used when registering new listener that should receive notification for all events
         * related to this paywall flow.
         */
        ALL: "ALL"
    };

    /**
     * Enumeration of known status values in the status field of response objects json object.
     * @enum {string}
     * @readonly
     * @global
     */
    global.PaywallResponseStatus = {
        /** Processing went ok, no exception occurred. */
        OK: "OK",
        /**
         * Invalid data was sent to service..
         */
        BAD_REQUEST: "BAD_REQUEST",
        /**
         * Temporary internal problems at the service. Possible to try again.
         */
        SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE",
        /**
         * Usually due to invalid token sent to service.
         */
        UNAUTHORIZED: "UNAUTHORIZED",
        /**
         * Internal error occurred at the service.
         */
        INTERNAL_SERVER_ERROR: "INTERNAL_SERVER_ERROR"
    };


    /**
     * Enumeration indicating the BTC unit that should be used when displaying and invoice amount.
     * @enum {string}
     * @readonly
     * @global
     */
    global.BTCUnit = {
        /** BTC, i.e 100.000.000 Satoshis */
        BTC: "BTC",
        /** One thousand part of BTC, i.e 100.000 Satoshis */
        MILLIBTC: "MILLIBTC",
        /**
         * In BIT, i.e 100 Satoshis.
         */
        BIT: "BIT",
        /**
         * In Satoshis.
         */
        SAT: "SAT",
        /**
         * In milli satoshis, 1/1000 satoshi.
         */
        MILLISAT: "MILLISAT",
        /**
         * In nano satoshis, 1/1000.000 satoshi.
         */
        NANOSAT: "NANOSAT"
    };

    /**
     * Internal Enum of used http statuses.
     * @enum {number}
     * @readonly
     * @global
     */
    var HttpStatus = {
        /** Payment is required notification. **/
        PAYMENT_REQUIRED : 402
    };

    /* test-code */
    // Define HttpStatus as enum available to test scripts.
    global.HttpStatus = HttpStatus;
    /* end-test-code */

    /**
     * Internal Enum of used http headers.
     * @enum {string}
     * @readonly
     * @global
     */
    var HttpHeader = {
        /** The related payload is a paywall related message. **/
        PAYWALL_MESSAGE : "PAYWALL_MESSAGE"
    };

    /* test-code */
    // Define HttpHeader as enum available to test scripts.
    global.HttpHeader = HttpHeader;
    /* end-test-code */

    /**
     * Internal Enum of Magnetudes used in JSON Amount objects.
     * @enum {number}
     * @readonly
     * @global
     */
    var Magnetude = {
        /** Base unit. Satoshis for BTC. **/
        NONE : "NONE",
        /** One thousand part of the base unit **/
        MILLI : "MILLI",
        /** One millionth part of the base unit **/
        NANO : "NANO"
    };

    /* test-code */
    // Define Magnetude as enum available to test scripts.
    global.Magnetude = Magnetude;
    /* end-test-code */

    /**
     * Internal Enum of Currency Codes used in JSON Amount objects.
     * @enum {number}
     * @readonly
     * @global
     */
    var CurrencyCode = {
        /** Bitcoin BTC **/
        BTC : "BTC"
    };

    /* test-code */
    // Define CurrencyCode as enum available to test scripts.
    global.CurrencyCode = CurrencyCode;
    /* end-test-code */


    /**
     * PaywallHttpRequest is the main class in this library. It's an paywall enhanced XMLHttpRequest what
     * wraps a standard XMLHttpRequest and checks if response is 402 (Payment Required). If that is the case
     * a web socket is automatically open to listen for settlement and later use that settlement token to automatically
     * perform the call again with the same parameters.
     *
     * @constructor PaywallHttpRequest
     */
    global.PaywallHttpRequest = function PaywallHttpRequest() {
        var invoice;
        var settlement;
        var paywallError;
        var executed = false;
        var aborted = false;

        var xmlHttpRequest = new XMLHttpRequest();

        var waitingInvoice = false;


        var xhrOpenData = {method: null, url: null, async: true, username: null, password: null,
            eventListeners:[], uploadEventListeners:[]};
        var xhrSendData = {body: null, requestHeaders: []};


        function getPaywallState() {
            if (paywallError !== undefined) {
                return PaywallState.PAYWALL_ERROR;
            }
            if (aborted){
                return PaywallState.ABORTED;
            }
            if (executed) {
                return PaywallState.EXECUTED;
            }
            if (invoice === undefined && settlement === undefined) {
                return PaywallState.NEW;
            }
            var now = Date.now();
            if (settlement === undefined) {
                if(new Date(invoice.invoiceExpireDate).getTime() < now){
                    // Invoice expired
                    return PaywallState.INVOICE_EXPIRED;
                }
                return PaywallState.INVOICE;
            } else {
                if(settlement.settlementValidFrom !== null){
                    if(new Date(settlement.settlementValidFrom).getTime() > now){
                        // Settlement not yet valid.
                        return PaywallState.SETTLEMENT_NOT_YET_VALID;
                    }
                }
                if(new Date(settlement.settlementValidUntil).getTime() < now){
                    // Settlement expired
                    return PaywallState.SETTLEMENT_EXPIRED;
                }
                return PaywallState.SETTLED;
            }
        }

        /**
         * Method to construct a full URL from a url value from invoice. It checks if url start with 'http', if not
         * it adds the window.location.origin before the given url value.
         * @param {String} url the url from invoice object, to QR, checkSettlement or WebSocket.
         * @return {String} the full url to the given end point.
         */
        function constructFullURL(url){
            if(url.startsWith("http")){
                return url;
            }
            return window.location.origin + url;
        }

        /**
         * Help method to cache registered event listeners for XMLHttpRequest.
         */
        function addEventListener(eventListenerList, type, callback, options){
            var index = findEventListenerIndex(eventListenerList, type);
            if(index === -1) {
                eventListenerList.push({type: type, callback: callback, options: options});
            }else{
                eventListenerList.splice(index,1,{type: type, callback: callback, options: options});
            }
        }

        /**
         * Help method to remove registered event listeners for XMLHttpRequest from cache.
         */
        function removeEventListener(eventListenerList, type) {
            var index = findEventListenerIndex(type);
            if(index !== -1){
                eventListenerList.splice(index,1);
            }
        }

        /**
         * Help method to find a specific event listener by type.
         *
         * @param eventListenerList the cache of event listeners, download or upload listeners.
         * @param type the type of event listener to find.
         * @return {number} index of found event listener by type
         */
        function findEventListenerIndex(eventListenerList,type ){
            for(var i=0; i<eventListenerList.length; i++){
                if(eventListenerList[i].type === type){
                    return i;
                }
            }
            return -1;
        }

        /**
         * Ready State handler set in XMLHttpRequest after settlement have been done.
         */
        function afterSettlementReadyStateHandler(){
            populateResponseAttributes();
            triggerOnReadyStateHandler();
        }

        /**
         * Paywall event listener that listens on all events for a SETTLED event and the
         * reinitializes the wrapped XMLHttpRequest and performs the API call again automatically.
         * @param type the type of event.
         * @param object the related object (settlement if type is settled).
         */
        var paywallOnReadyStateChangeListener = function (type, object) {
            if(type === PaywallEventType.SETTLED){
                paywallWebSocketHandler.close();
                waitingInvoice = false;
                settlement = object;
                xmlHttpRequest = new XMLHttpRequest();
                xmlHttpRequest.onreadystatechange = afterSettlementReadyStateHandler;
                populateAllEventListeners();

                if(xhrOpenData.async === undefined) {
                    xmlHttpRequest.open(xhrOpenData.method,xhrOpenData.url);
                }else{
                    xmlHttpRequest.open(xhrOpenData.method,xhrOpenData.url,
                        xhrOpenData.async, xhrOpenData.username, xhrOpenData.password);
                }
                populateRequestAttributes();
                setTokenHeader();

                xmlHttpRequest.send(xhrSendData.body);
            }
        };

        /**
         * On ready state handler for wrapped XMLHttpRequest that checks if response has PAYMENT_REQUIRED
         * and then open up a websocket to wait for settlement.
         */
        function onReadyStateHandler() {
            if(!waitingInvoice) {
                if (xmlHttpRequest.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
                    if (xmlHttpRequest.status === HttpStatus.PAYMENT_REQUIRED) {
                        waitingInvoice = true;
                    } else {
                        populateResponseAttributes();
                        populateAllEventListeners();
                        triggerOnReadyStateHandler();
                    }
                } else {
                    populateResponseAttributes();
                    triggerOnReadyStateHandler();
                    if(hasPaywallErrorOccurred()){
                        handlePaywallError();
                    }
                }
            }else{
                if(xmlHttpRequest.readyState === XMLHttpRequest.DONE) {
                    if(!hasPaywallErrorOccurred()) {
                        invoice = JSON.parse(xmlHttpRequest.responseText);
                        paywallEventBus.triggerEventFromState();
                        paywallEventBus.addListenerFirst("OnReadyStateListener", PaywallEventType.ALL, paywallOnReadyStateChangeListener);
                        paywallWebSocketHandler.connect(invoice);
                    }else{
                        handlePaywallError();
                    }
                }
            }
        }

        /**
         * Help method that checks if ready state is DONE and related message is a paywall related
         * error.
         * @return {boolean} if paywall related error has occurred.
         */
        function hasPaywallErrorOccurred(){
            if(xmlHttpRequest.readyState === XMLHttpRequest.DONE){
                if(xmlHttpRequest.getResponseHeader(HttpHeader.PAYWALL_MESSAGE) === "TRUE"){
                    var errorType = xmlHttpRequest.status / 100;
                    if(errorType === 4 || errorType === 5 ){
                        return true;
                    }
                }
            }
            return false;
        }

        /**
         * Help method that parses the related response text as a paywall error and
         * triggers related event.
         */
        function handlePaywallError(){
            paywallError = JSON.parse(xmlHttpRequest.responseText);
            paywallEventBus.onEvent(PaywallEventType.PAYWALL_ERROR, paywallError);
        }

        /**
         * Method that trigger ready state handler in underlying XMLHttpRequest object.
         * It also checks if related payment is pay per request and sets the state as executed
         * if download was successful.
         */
        function triggerOnReadyStateHandler() {

            if(api.onstatechange !== undefined){
                api.readyState = xmlHttpRequest.readyState;
                api.onstatechange();
            }

            if(xmlHttpRequest.readyState === XMLHttpRequest.DONE &&
                settlement !== undefined && settlement.payPerRequest){
                var status = xmlHttpRequest.status / 100;
                if(status === 2) { // If no error occurred.
                    executed = true;
                    paywallEventBus.onEvent(PaywallEventType.EXECUTED, settlement);
                }
            }
        }

        /**
         * Help method to populate all base eventlisteners that should be populated
         * in underlying XMLHttpRequest object from start. (Before determining if this is
         * a paywalled request or not). The event handler populated are timeout, abort and error.
         */
        function populateBaseEventListeners() {
            xmlHttpRequest.ontimeout = api.ontimeout;
            xmlHttpRequest.onabort = api.onabort;
            xmlHttpRequest.onerror = api.onerror;

            for(var i=0; i <xhrOpenData.eventListeners.length ; i++){
                var listener = xhrOpenData.eventListeners[i];
                if(listener.type === "timeout"  || listener.type === "abort" || listener.type === "error" ) {
                    xmlHttpRequest.addEventListener(listener.type, listener.callback, listener.options);
                }
            }

            if(xmlHttpRequest.upload !== undefined){
                xmlHttpRequest.upload.ontimeout = api.upload.ontimeout;
                xmlHttpRequest.upload.onabort = api.upload.onabort;
                xmlHttpRequest.upload.onerror = api.upload.onerror;

                for(var j=0; j <xhrOpenData.uploadEventListeners.length ; j++){
                    var listener1 = xhrOpenData.uploadEventListeners[j];
                    if(listener1.type === "timeout"  || listener1.type === "abort" || listener1.type === "error" ) {
                        xmlHttpRequest.upload.addEventListener(listener1.type, listener1.callback, listener1.options);
                    }
                }
            }

        }

        /**
         * Help method that populates all event handlers to the underlying XMLHttpRequest object
         * after payment have been settled.
         *
         */
        function populateAllEventListeners() {
            xmlHttpRequest.onloadstart = api.onloadstart;
            xmlHttpRequest.onload = api.onload;
            xmlHttpRequest.onprogress = api.onprogress;
            xmlHttpRequest.onabort = api.onabort;
            xmlHttpRequest.onerror = api.onerror;
            xmlHttpRequest.ontimeout = api.ontimeout;
            xmlHttpRequest.onloadend = api.onloadend;

            for(var i=0; i <xhrOpenData.eventListeners.length ; i++){
                var listener = xhrOpenData.eventListeners[i];
                xmlHttpRequest.addEventListener(listener.type, listener.callback, listener.options);
            }

            if(xmlHttpRequest.upload !== undefined){
                xmlHttpRequest.upload.onloadstart = api.upload.onloadstart;
                xmlHttpRequest.upload.onload = api.upload.onload;
                xmlHttpRequest.upload.onprogress = api.upload.onprogress;
                xmlHttpRequest.upload.onabort = api.upload.onabort;
                xmlHttpRequest.upload.onerror = api.upload.onerror;
                xmlHttpRequest.upload.ontimeout = api.upload.ontimeout;
                xmlHttpRequest.upload.onloadend = api.upload.onloadend;

                for(var j=0; j <xhrOpenData.uploadEventListeners.length ; j++){
                    var listener1 = xhrOpenData.uploadEventListeners[j];
                    xmlHttpRequest.upload.addEventListener(listener1.type, listener1.callback, listener1.options);
                }
            }
        }

        /**
         * Help method that populates all request attributes before performing a call to the service.
         */
        function populateRequestAttributes() {
            xmlHttpRequest.timeout = api.timeout;
            xmlHttpRequest.withCredentials = api.withCredentials;
        }

        /**
         * Help method that sets the settlement token in as a request header if state is SETTLED.
         */
        function setTokenHeader(){
            if(getPaywallState() === PaywallState.SETTLED){
                xmlHttpRequest.setRequestHeader("Payment", api.paywall.getSettlement().token);
            }
        }

        /**
         * Help method to populate response attribute from the underlying XMLHttpRequest object.
         * @param {boolean} onlyStatus if only status related attributes should be populated and not the
         * actual response.
         */
        function populateResponseAttributes(onlyStatus) {
            api.status = xmlHttpRequest.status;
            api.statusText = xmlHttpRequest.statusText;
            api.responseURL = xmlHttpRequest.responseURL;
            if(onlyStatus === undefined || onlyStatus === false){
                api.responseType = xmlHttpRequest.responseType;
                api.response = xmlHttpRequest.response;
                api.responseText = xmlHttpRequest.responseText;
                api.responseXML = xmlHttpRequest.responseXML;
            }
        }

        var api = {
            timeout : 0,
            withCredentials : false,

            /**
             * Upload Event XMLHttpRequestEventTarget containing event listeners for upload events.
             * Only called after passing paywall.
             *
             * @memberof PaywallHttpRequest
             * @namespace PaywallHttpRequest.upload
             */
            upload: {
                onload: undefined,
                onloadstart: undefined,
                onloadend: undefined,
                onerror: undefined,
                onprogress: undefined,
                onstatechange: undefined,
                ontimeout: undefined,

                /**
                 * Method to add upload event listener. The listener is cached until
                 * passing paywall. See standard EventTarget documentation for details.
                 * @param type type of event.
                 * @param callback the call back function.
                 * @param options callback options, see EventTarget documentation.
                 * @memberof PaywallHttpRequest.upload
                 */
                addEventListener : function(type, callback, options){
                    addEventListener(xhrOpenData.uploadEventListeners,type,callback,options);
                },

                /**
                 * Method to remove upload event listener. The listener is cached until
                 * passing paywall. See standard EventTarget documentation for details.
                 * @param type type of event.
                 * @param callback the call back function.
                 * @param options callback options, see EventTarget documentation.
                 * @memberof PaywallHttpRequest.upload
                 */
                removeEventListener : function(type, callback, options){
                    removeEventListener(xhrOpenData.uploadEventListeners,type);
                },

                /**
                 * Method that shouldn't be called will throw error.
                 * @memberof PaywallHttpRequest.upload
                 */
                dispatchEvent : function(event){
                    throw("Internal dispatchEvent should never be called.");
                }
            },

            readyState: XMLHttpRequest.UNSENT,

            // response attributes
            responseURL : undefined,
            status: undefined,
            statusText: undefined,
            responseType : undefined,
            response: undefined,
            responseText: undefined,
            responseXML: undefined,

            // Events
            onload: undefined,
            onloadstart: undefined,
            onloadend: undefined,
            onerror: undefined,
            onprogress: undefined,
            onstatechange: undefined,
            ontimeout: undefined,

            /**
             * Method to add download event listener. The listener is cached until
             * passing paywall. See standard EventTarget documentation for details.
             * @param type type of event.
             * @param callback the call back function.
             * @param options callback options, see EventTarget documentation.
             * @memberof PaywallHttpRequest
             */
            addEventListener : function(type, callback, options){
                addEventListener(xhrOpenData.eventListeners,type,callback,options);
            },

            /**
             * Method to remove download event listener. The listener is cached until
             * passing paywall. See standard EventTarget documentation for details.
             * @param type type of event.
             * @param callback the call back function.
             * @param options callback options, see EventTarget documentation.
             * @memberof PaywallHttpRequest
             */
            removeEventListener : function(type, callback, options){
                removeEventListener(xhrOpenData.eventListeners,type);
            },

            /**
             * Method that shouldn't be called will throw error.
             * @memberof PaywallHttpRequest
             */
            dispatchEvent : function(event){
                throw("Internal dispatchEvent should never be called.");
            },


            /**
             * Method to abort the call and close all underlying resources such as event bus
             * and web socket.
             * @memberof PaywallHttpRequest
             */
            abort : function(){
              aborted = true;
              paywallEventBus.close();
              paywallWebSocketHandler.close();
              xmlHttpRequest.abort();
            },

            // HERE
            /**
             * Method to set the related request header in underlying XMLHttpRequest object.
             * The value is cached so subsequent call after passing paywall it is set again
             * automatically.
             * @param name request header name.
             * @param value request header value.
             * @memberof PaywallHttpRequest
             */
            setRequestHeader : function(name, value){
              xhrSendData.requestHeaders.push({name: name, value: value});
            },

            /**
             * Method to open a connection to given URL and initializes underlying resources
             * for paywall handling.
             *
             * @param method http method to use
             * @param url url to connecto to
             * @param async if call should be asynchronical (default true), optional.
             * @param username username to use with the call, optional
             * @param password password to use with the call, optional
             * @memberof PaywallHttpRequest
             */
            open : function(method, url, async, username, password){
                // Reset send data
                xhrOpenData.method = method;
                xhrOpenData.url = url;
                xhrSendData = {body: null, requestHeaders: []};
                xmlHttpRequest.onreadystatechange = onReadyStateHandler;
                populateBaseEventListeners();
                if(async === undefined) {
                    xmlHttpRequest.open(method, url);
                }else{
                    xhrOpenData.async = async;
                    xhrOpenData.username = username;
                    xhrOpenData.password = password;
                    xmlHttpRequest.open(method, url, async, username, password);
                }
            },

            /**
             * Method to send the request, data is cached to be sent again automatically
             * after payment is settled.
             * @param body optional body data to upload.
             * @memberof PaywallHttpRequest
             */
            send : function(body){
                xhrSendData.body = body;
                populateRequestAttributes();
                populateBaseEventListeners();
                for(var i=0;i<xhrSendData.requestHeaders.length;i++){
                    var requestHeader = xhrSendData.requestHeaders[0];
                    xmlHttpRequest.setRequestHeader(requestHeader.name, requestHeader.value);
                }
                setTokenHeader();
                xmlHttpRequest.send(body);
            },


            /**
             * Method to fetch response header for underlying XMLHttpRequest object.
             * @param name of resonse header.
             * @return {string} response header value.
             * @memberof PaywallHttpRequest
             */
            getResponseHeader : function(name){
                return xmlHttpRequest.getResponseHeader(name);
            },

            /**
             * Method to fetch all response headers from underlying XMLHttpRequest object.
             * @return {string} all response headers value.
             * @memberof PaywallHttpRequest
             */
            getAllResponseHeaders: function(){
                return xmlHttpRequest.getAllResponseHeaders();

            },

            /**
             * Method to override the default mime type sent in response.
             * @param mime mine-type to override with.
             * @memberof PaywallHttpRequest
             */
            overrideMimeType : function(mime){
                return xmlHttpRequest.overrideMimeType(mime);
            },

            /**
             * Paywall section containing paywall extensions to standard XMLHttpRequest methods.
             * @memberof PaywallHttpRequest
             * @namespace PaywallHttpRequest.paywall
             */
            paywall : {

                /**
                 * Method to retrieve invoice if exists in related payment flow.
                 * @returns {Object} related invoice if generated in payment flow, otherwise undefined.
                 * @memberof PaywallHttpRequest.paywall
                 */
                getInvoice: function () {
                    return invoice;
                },
                /**
                 * Returns true if invoice exists in related payment flow.
                 * @returns {boolean} true if invoice exist
                 * @memberof PaywallHttpRequest.paywall
                 */
                hasInvoice: function () {
                    return invoice !== undefined;
                },
                /**
                 * Help method to construct a PaywallTime object of invoice expiration date, used to
                 * simply output remaining hours, minutes, etc of invoice.
                 *
                 * @returns {PaywallTime} if invoice exist
                 * @throws error if no invoice currently exists in payment flow.
                 * @memberof PaywallHttpRequest.paywall
                 */
                getInvoiceExpiration: function () {
                    if (invoice !== undefined) {
                        return new PaywallTime(invoice.invoiceExpireDate);
                    }
                    throw("Invalid state " + getPaywallState() + " when calling method getInvoiceExpiration().");
                },
                /**
                 * Help method to construct to retrieve a  PaywallAmount object of invoice amount that
                 * is easy converted by a specified unit.
                 *
                 * @see PaywallAmount
                 * @returns {PaywallAmount} if invoice exist
                 * @throws error if no invoice currently exists in payment flow.
                 * @memberof PaywallHttpRequest.paywall
                 */
                getInvoiceAmount: function () {
                    if (invoice !== undefined) {
                        return new PaywallAmount(invoice.invoiceAmount);
                    }
                    throw("Invalid state " + getPaywallState() + " when calling method getInvoiceAmount().");
                },

                /**
                 * Method to retrieve settlement if exists in related payment flow.
                 * @returns {Object} related settlement if generated in payment flow, otherwise undefined.
                 * @memberof PaywallHttpRequest.paywall
                 */
                getSettlement: function () {
                    return settlement;
                },
                /**
                 * Returns true if settlement exists in related payment flow.
                 * @returns {boolean} true if settlement exist
                 * @memberof PaywallHttpRequest.paywall
                 */
                hasSettlement: function () {
                    return settlement !== undefined;
                },
                /**
                 * Help method to construct a PaywallTime object of settlement expiration date, used to
                 * simply output remaining hours, minutes, etc of settlement validity.
                 *
                 * @returns {PaywallTime} if settlement exist
                 * @throws error if no settlement currently exists in payment flow. It is best to check
                 * this before calling method.
                 * @memberof PaywallHttpRequest.paywall
                 */
                getSettlementExpiration: function () {
                    if (settlement !== undefined) {
                        return new PaywallTime(settlement.settlementValidUntil);
                    }
                    throw("Invalid state " + getPaywallState() + " when calling method getSettlementExpiration().");
                },
                /**
                 * Help method to construct a PaywallTime object of settlement valid from date, used to
                 * simply output remaining hours, minutes, etc until settlement validity. If no validFrom
                 * field is set in settlement is current date returned.
                 *
                 * @returns {PaywallTime} if settlement exist
                 * @throws error if no settlement currently exists in payment flow. It is best to check
                 * this before calling method.
                 * @memberof PaywallHttpRequest.paywall
                 */
                getSettlementValidFrom: function () {
                    if (settlement !== undefined) {
                        if (settlement.settlementValidFrom != null) {
                            return new PaywallTime(settlement.settlementValidFrom);
                        }
                        return new PaywallTime(new Date().toDateString());
                    }
                    throw("Invalid state " + getPaywallState() + " or when calling method getSettlementValidFrom().");
                },
                /**
                 * Help method returning the full URL to the QR Code generation link. The invoice object can return
                 * a relative url and this help method always ensures a full URL is returned.
                 *
                 * @return {String} full URL to the QR Code generation link.
                 */
                genQRLink: function(){
                    if(invoice !== undefined){
                        return constructFullURL(invoice.qrLink);
                    }
                    throw("Invalid state " + getPaywallState() + " or when calling method genQRLink().");
                },
                /**
                 * Help method returning the full URL to the Check Settlement Endpoint link. The invoice object can return
                 * a relative url and this help method always ensures a full URL is returned.
                 *
                 * @return {String} full URL to the Check Settlement Endpoint link.
                 */
                genCheckSettlementLink: function(){
                    if(invoice !== undefined){
                        return constructFullURL(invoice.checkSettlementLink);
                    }
                    throw("Invalid state " + getPaywallState() + " or when calling method genCheckSettlementLink().");
                },
                /**
                 * Help method returning the full URL to the Check Settlement WebSocket Endpoint link. The invoice
                 * object can return a relative url and this help method always ensures a full URL is returned.
                 *
                 * @return {String} full URL to the Check Settlement WebSocket Endpoint link.
                 */
                genCheckSettlementWebSocketLink: function(){
                    if(invoice !== undefined){
                        return constructFullURL(invoice.checkSettlementWebSocketEndpoint);
                    }
                    throw("Invalid state " + getPaywallState() + " or when calling method genCheckSettlementWebSocketLink().");
                },
                /**
                 * Method to retrieve error object containing error information if paywall related error occurred during payment flow.
                 * @returns {Object} related error if generated in payment flow, otherwise undefined.
                 * @memberof PaywallHttpRequest.paywall
                 */
                getPaywallError: function () {
                    return paywallError;
                },
                /**
                 * Method to retrieve current state of payment flow, will return one of PaywallState enums.
                 * State EXPIRED will be returned both if invoice or settlement have expired. If state is EXPIRED
                 * and settlement is null, then it's the invoice that have expired.
                 *
                 * @return {string} one of PaywallState enumeration values.
                 * @memberof PaywallHttpRequest.paywall
                 */
                getState: getPaywallState,

                /**
                 * Method to add a listener, if listener already exists with given name it will be updated.
                 * Multiple listeners for the same type is supported.
                 *
                 * @param {string} name the unique name of the listener within this payment flow.
                 * @param {PaywallEventType} type the type of event to listen to, or special ALL that receives all events.
                 * @param {function} callback method that should be called on given event. The function should have two parameters
                 * one PaywallEventType and one object containing the object data. Type of object differs for each event.
                 * @memberof PaywallHttpRequest.paywall
                 */
                addEventListener: function (name, type, callback) {
                    paywallEventBus.addListener(name, type, callback);
                },

                /**
                 * Method to remove listener with given name if exists.
                 * @param {string} name the name of listener to remove.
                 * @memberof PaywallHttpRequest.paywall
                 */
                removeEventListener: function (name) {
                    paywallEventBus.removeListener(name);
                }
            }

        };

        // Define eventBus that is dependant on API object.
        var paywallEventBus = new PaywallEventBus(api);
        var paywallWebSocketHandler = new PaywallWebSocket(api,paywallEventBus);


        /* test-code */
        // Help code to access private fields during unit tests.
        api.setPaywallInvoice = function (inv) {
            invoice = inv;
        };
        api.setPaywallSettlement = function (setl) {
            settlement = setl;
        };
        api.setPaywallError = function (err) {
            paywallError = err;
        };
        api.setPaywallExecuted = function (exec) {
            executed = exec;
        };
        api.getPaywallExecuted = function () {
            return executed;
        };
        api.getPaywallEventBus = function (){
            return paywallEventBus;
        };
        api.setPaywallEventBus = function (eventBus){
            paywallEventBus = eventBus;
        };
        api.getPaywallWebSocketHandler = function (){
            return paywallWebSocketHandler;
        };
        api.setPaywallWebSocketHandler = function (webSocketHandler){
            paywallWebSocketHandler = webSocketHandler;
        };
        api.getXhrOpenData = function () {
            return xhrOpenData;
        };

        api.getXhrSendData = function () {
            return xhrSendData;
        };

        api.setXMLHttpRequest = function(mockedRequest) {
            xmlHttpRequest = mockedRequest;
        };
        /* end-test-code */

        return api;
    };

    /**
     * Helper class to calculate and present remaining validity or time until valid
     * of an invoice or an settlement.
     *
     * @param {string} timeStamp the timestamp to parse.
     * @constructor PaywallTime
     */
    global.PaywallTime = function PaywallTime(timeStamp) {
        var date = new Date(timeStamp);

        var api = {
            /**
             *
             * @returns {Date} returns the related timestamp as a Date object.
             * @memberof PaywallTime
             */
            getTimeStamp : function () {
                return date;
            },
            /**
             * Help method to get the difference between timestamp and current time. If
             * current time is after timestamp is 0 returned, never a negative number.
             * Used to indicate the remaining time of a invoice or settlement.
             * @returns a PaywallTimeUnit with the difference between given timestamp and current time.
             * @memberof PaywallTime
             */
            remaining : function () {
                return new PaywallTimeUnit(date.getTime() - Date.now());
            }
        };

        /* test-code */
        // Help code to access private fields during unit tests.

        /* end-test-code */

        return api;
    };

    /**
     * PaywallTimeUnit is a help class to return remaining time in time units
     * such as seconds, minutes, days etc.
     *
     * With this object it is possible to easy display a string of remaining
     * or time until in form HH:MM:SS etc.
     *
     * @param {number} timeInMS time difference in milliseconds, if less than 0 it will be set to 0.
     * @constructor PaywallTimeUnit
     */
    global.PaywallTimeUnit = function PaywallTimeUnit(timeInMS) {

        if(timeInMS < 0){
            timeInMS=0;
        }

        var addPadding = function(value){
            if(value < 10){
                return "0" + value;
            }
            return "" + value;
        };

        function seconds() {
            return addPadding(Math.floor((timeInMS % (1000 * 60)) / 1000));
        }

        function minutes() {
            return addPadding(Math.floor((timeInMS % (1000 * 60 * 60)) / (1000 * 60)));
        }

        function hours() {
            return addPadding(Math.floor((timeInMS % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)));
        }

        function days() {
            return "" + Math.floor(timeInMS / (1000 * 60 * 60 * 24));
        }

        var api = {
            /**
             *
             * @returns {number} number of milliseconds of remaining time. This is the total number
             * of milliseconds not from the last second and is not padded.
             * @function asMS
             * @memberof PaywallTimeUnit
             */
            asMS : function () {
                return timeInMS;
            },
            /**
             * Help method to display the seconds part of remaining time. The method returns
             * the number of seconds in addition to remaining minutes.
             * @returns {string} remaining time in seconds as two characters. i.e 01 or 12
             * @function seconds
             * @memberof PaywallTimeUnit
             */
            seconds : seconds,
            /**
             * Help method to display the minutes part of remaining time. The method returns
             * the number of minutes in addition to remaining hours.
             * @returns {string} remaining time in minutes as two characters. i.e 01 or 12
             * @function minutes
             * @memberof PaywallTimeUnit
             */
            minutes : minutes,
            /**
             * Help method to display the hours part of remaining time. The method returns
             * the number of hours in addition to remaining days.
             * @returns {string} remaining time in hours as two characters. i.e 01 or 12
             * @function hours
             * @memberof PaywallTimeUnit
             */
            hours : hours,
            /**
             * Method that returns the remaining days.
             *
             * @returns {number} remaining time in days without padding.
             * @function days
             * @memberof PaywallTimeUnit
             */
            days : days
        };

        /* test-code */
        // Help code to access private fields during unit tests.

        /* end-test-code */

        return api;
    };

    /**
     * PaywallAmount is a help class to display invoiced amount in specified unit.
     *
     * With this object it is possible to easy display convert invoiced amount
     * in specified unit.
     *
     * @param {object} amount json object from invoice.
     * @constructor PaywallAmount
     */
    global.PaywallAmount = function PaywallAmount(amount) {

        function normalizeAmount(){
            if(amount.value === undefined){
                throw "Invalid invoice amount object in PaywallAmount.";
            }
            if(amount.currencyCode !== undefined && amount.currencyCode !== CurrencyCode.BTC){
                throw "Invalid invoice currency code " + amount.currencyCode + " in PaywallAmount, currently only BTC is supported.";
            }
            var magnetude = Magnetude.NONE;
            if(amount.magnetude !== undefined){
                magnetude = amount.magnetude;
            }
            switch (magnetude) {
                case Magnetude.NONE:
                    return amount.value;
                case Magnetude.MILLI:
                    return amount.value / 1000;
                case Magnetude.NANO:
                    return amount.value / 1000000;
            }
            throw "Invalid magnetude from invoice " + magnetude  + " in PaywallAmount.";
        }

        var api = {
            /**
             * Method to retrieve the specified amount in specified unit, currently is only BTCUnit enum values specified.
             * @see BTCUnit
             * @param unit the unit that the amount should be displayed as. Currently is only BTCUnit defined units supported
             * @returns {number} the amount in specified unit.
             * @function as
             * @memberof PaywallAmount
             */
            as : function (unit) {
                var sats = normalizeAmount();
                switch (unit) {
                    case BTCUnit.BTC:
                        return sats / 100000000;
                    case BTCUnit.MILLIBTC:
                        return sats / 100000;
                    case BTCUnit.BIT:
                        return sats / 100;
                    case BTCUnit.SAT:
                        return sats;
                    case BTCUnit.MILLISAT:
                        return sats * 1000;
                    case BTCUnit.NANOSAT:
                        return sats * 1000000;
                    default:
                        throw "Unsupported unit " + unit + " used with PaywallAmount, only BTCUnit enumerated values are supported";
                }
            }
        };

        /* test-code */
        // Help code to access private fields during unit tests.

        /* end-test-code */

        return api;
    };


    /**
     * Private class in charge of maintaining all listeners for a in a payment flow.
     *
     * It allows for registration and un-registration of a listener and onEvent forwards
     * the event to all matching listeners.
     *
     * @param {Paywall|object} paywallHttpRequest the related payment flow.
     * @constructor PaywallEventBus
     */
    function PaywallEventBus(paywallHttpRequest) {
        var listeners = [];
        var currentState = paywallHttpRequest.paywall.getState();


        function checkStateTransition(){
            var newState = paywallHttpRequest.paywall.getState();
            if(currentState !== newState){
                currentState = newState;
                if(newState !== PaywallState.SETTLED) {
                    if (isFinalState(newState)) {
                        clearInterval(stateChecker);
                    }
                    onEvent(getRelatedType(newState), getRelatedObject(newState));
                }
            }
        }

        var stateChecker = setInterval(checkStateTransition, 1000);

        /**
         * Method to close underlying resources and background check.
         * @memberof PaywallEventBus
         */
        var close = function(){
            clearInterval(stateChecker);
        };
        this.close = close;

        /**
         * Method to add a listener, if listener already exists with given name it will be updated.
         * @param {string} name the unique name of the listener within this payment flow.
         * @param {PaywallEventType} type the type of event to listen to, or special ALL that receives all events.
         * @param {function} callback method that should be called on given event. The function should have two parameters
         * one PaywallEventType and one object containing the object data. Type of object differs for each event.
         * @memberof PaywallEventBus
         */
        var addListener = function(name, type, callback) {
            var index = findIndex(name);
            if(index === -1) {
                listeners.push({name: name, type: type, onEvent: callback});
            }else{
                listeners.splice(index,1,{name: name, type: type, onEvent: callback});
            }
        };
        this.addListener = addListener;

        /**
         * Method to add a listener to first position in eventBus, if listener already exists with given name it will be removed and the new callback
         * will be added first.
         * @param {string} name the unique name of the listener within this payment flow.
         * @param {PaywallEventType} type the type of event to listen to, or special ALL that receives all events.
         * @param {function} callback method that should be called on given event. The function should have two parameters
         * one PaywallEventType and one object containing the object data. Type of object differs for each event.
         * @memberof PaywallEventBus
         */
        var addListenerFirst = function(name, type, callback) {
            var index = findIndex(name);
            if(index !== -1) {
                listeners.splice(index,1);
            }
            listeners.unshift({name: name, type: type, onEvent: callback});
        };
        this.addListenerFirst = addListenerFirst;

        /**
         * Method to remove listener with given name if exists.
         * @param {string} name the name of listener to remove.
         * @memberof PaywallEventBus
         */
        var removeListener = function(name) {
            var index = findIndex(name);
            if(index !== -1){
                listeners.splice(index,1);
            }
        };

        this.removeListener = removeListener;
        /**
         * Method called when a given event occurred and the method forwards the event
         * to a listeners that matches.
         * @param {PaywallEventType} type the type of event that has been triggered.
         * @param {*} object related data object, different data depending on event type. For instance
         * event type INVOICE will contain the invoice etc.
         * @memberof PaywallEventBus
         */
        var onEvent = function(type, object) {
            currentState = paywallHttpRequest.paywall.getState();
            var matchingListeners = listeners.filter(function(item){
                return item.type === type || item.type === PaywallEventType.ALL;});
            for(var i=0;i<matchingListeners.length;i++){
                matchingListeners[i].onEvent(type, object);
            }
        };

        this.onEvent = onEvent;

        /**
         * Method to trigger an event from the current status of the payment flow
         * the event type and related object will be calculated automatically.
         * @memberof PaywallEventBus
         */
        var triggerEventFromState = function () {
            var state = paywallHttpRequest.paywall.getState();
            onEvent(getRelatedType(state),getRelatedObject(state));
        };
        this.triggerEventFromState = triggerEventFromState;

        function findIndex(name){
            for(var i=0; i<listeners.length; i++){
                if(listeners[i].name === name){
                    return i;
                }
            }
            return -1;
        }

        /**
         * Help method to determine if background state checker should still be
         * run or if it can be ended.
         * @param {PaywallState|string} state the current state
         * @return {boolean} true if state is final and won't be changed in the future.
         */
        function isFinalState(state) {
            switch (state) {
                case PaywallState.NEW:
                case PaywallState.INVOICE:
                case PaywallState.SETTLEMENT_NOT_YET_VALID:
                case PaywallState.SETTLED:
                    return false;
            }
            return true;
        }

        /**
         * Help method to retrieve related event type for a given state.
         * @param {PaywallState|string} state the state to convert.
         * @return {PaywallEventType|string} the related event type.
         */
        function getRelatedType(state){
            switch (state) {
                case PaywallState.INVOICE:
                    return PaywallEventType.INVOICE;
                case PaywallState.INVOICE_EXPIRED:
                    return PaywallEventType.INVOICE_EXPIRED;
                case PaywallState.SETTLED:
                    return PaywallEventType.SETTLED;
                case PaywallState.EXECUTED:
                    return PaywallEventType.EXECUTED;
                case PaywallState.SETTLEMENT_NOT_YET_VALID:
                    return PaywallEventType.SETTLEMENT_NOT_YET_VALID;
                case PaywallState.SETTLEMENT_EXPIRED:
                    return PaywallEventType.SETTLEMENT_EXPIRED;
                case PaywallState.PAYWALL_ERROR:
                    return PaywallEventType.PAYWALL_ERROR;
                case PaywallState.API_ERROR:
                    return PaywallEventType.API_ERROR;
            }
            throw "Invalid state sent to Paywall EventBus: " + state;
        }

        /**
         * Help method to retrieve related object for a given state sent to event listeners.
         * @param {PaywallState|string} state the state to return related object of.
         * @return {*} state related data, either invoice, settlement or error.
         */
        function getRelatedObject(state){
            switch (state) {
                case PaywallState.INVOICE:
                    return paywallHttpRequest.paywall.getInvoice();
                case PaywallState.INVOICE_EXPIRED:
                    return paywallHttpRequest.paywall.getInvoice();
                case PaywallState.SETTLED:
                    return paywallHttpRequest.paywall.getSettlement();
                case PaywallState.EXECUTED:
                    return paywallHttpRequest.paywall.getSettlement();
                case PaywallState.SETTLEMENT_NOT_YET_VALID:
                    return paywallHttpRequest.paywall.getSettlement();
                case PaywallState.SETTLEMENT_EXPIRED:
                    return paywallHttpRequest.paywall.getSettlement();
                case PaywallState.PAYWALL_ERROR:
                    return paywallHttpRequest.paywall.getPaywallError();
            }
            throw "Invalid state sent to Paywall EventBus: " + state;
        }

        /* test-code */
        // Help code to access private fields during unit tests.

          this.getListeners = function () {
              return listeners;
          };

          this.isFinalState = isFinalState;
          this.getRelatedType = getRelatedType;
          this.getRelatedObject = getRelatedObject;
          this.getCurrentState = function(){
              return currentState;
          };
        /* end-test-code */
    }


    /* test-code */
    // Define PaywallEventBuss as class available to test scripts.
    global.PaywallEventBus = PaywallEventBus;
    /* end-test-code */

    /**
     * Private class in charge of maintaining WebSocket connection and callbacks
     * to the related payment flow.
     *
     * @param {Paywall|object} paywall the related payment flow.
     * @param {PaywallEventBus|object} eventBus the related payment flow event bus.
     * @constructor PaywallWebSocket
     */
    function PaywallWebSocket(paywall, eventBus) {

        var socket;
        var stompSocket;

        function processWebSocketMessage(message){
            if(message.body){
                var object = JSON.parse(message.body);
                if(object.status === PaywallResponseStatus.OK){
                    var eventType = getSettledStatus(object);
                    console.debug("Paywall WebSocket, received message if type: " + eventType);
                    eventBus.onEvent(eventType,object);
                }else{
                    console.debug("Paywall WebSocket, received error message: " + object);
                    eventBus.onEvent(PaywallEventType.PAYWALL_ERROR,object);
                }
            }else{
                console.debug("Paywall WebSocket, received empty message, ignoring.");
            }
        }

        function processWebSocketError(error){
            var errorObject;
            if(error.headers !== undefined){
                errorObject = {status: PaywallResponseStatus.SERVICE_UNAVAILABLE,
                    message: error.headers.message,
                    errors: [error.headers.message]
                };
            }else{
                errorObject = {status: PaywallResponseStatus.SERVICE_UNAVAILABLE,
                    message: error,
                    errors: [error  ]
                };
            }

            console.debug("Paywall WebSocket connection error: " + errorObject);
            eventBus.onEvent(PaywallEventType.PAYWALL_ERROR,errorObject);
        }

        function getSettledStatus(settlement){
            var now = Date.now();
            if(settlement.settlementValidFrom !== null){
                if(new Date(settlement.settlementValidFrom).getTime() > now){
                    // Settlement not yet valid.
                    return PaywallEventType.SETTLEMENT_NOT_YET_VALID;
                }
            }
            if(new Date(settlement.settlementValidUntil).getTime() < now){
                // Settlement expired
                return PaywallEventType.SETTLEMENT_EXPIRED;
            }
            return PaywallEventType.SETTLED;
        }



        /**
         * Method to close underlying resources and background check.
         * @param {object} invoice the newly generated invoice.
         * @memberof PaywallWebSocket
         */
        var connect = function(invoice){
            socket = new SockJS(paywall.paywall.genCheckSettlementWebSocketLink());
            stompSocket = Stomp.over(socket);

            var headers = {"token": invoice.token};
            stompSocket.connect({}, function(frame) {
                stompSocket.subscribe(invoice.checkSettlementWebSocketQueue, processWebSocketMessage, headers);
            }, processWebSocketError);
        };
        this.connect = connect;

        /**
         * Method to close underlying WebSocket.
         * @memberof PaywallWebSocket
         */
        var close = function(){
            if(stompSocket !== undefined){
                console.debug("Paywall WebSocket, closing connection.");
                stompSocket.disconnect();
                socket.close();
            }
        };
        this.close = close;



        /* test-code */
        // Help code to access private fields during unit tests.
        this.processWebSocketMessage = processWebSocketMessage;
        this.processWebSocketError = processWebSocketError;
        /* end-test-code */
    }


    /* test-code */
    // Define PaywallWebSocket as class available to test scripts.
    global.PaywallWebSocket = PaywallWebSocket;
    /* end-test-code */

})(this);