// https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
if (!Object.assign) {
  Object.defineProperty(Object, 'assign', {
    enumerable: false,
    configurable: true,
    writable: true,
    value: function(target, firstSource) {
      'use strict';
      if (target === undefined || target === null) {
        throw new TypeError('Cannot convert first argument to object');
      }

      var to = Object(target);
      for (var i = 1; i < arguments.length; i++) {
        var nextSource = arguments[i];
        if (nextSource === undefined || nextSource === null) {
          continue;
        }

        var keysArray = Object.keys(Object(nextSource));
        for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
          var nextKey = keysArray[nextIndex];
          var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
          if (desc !== undefined && desc.enumerable) {
            to[nextKey] = nextSource[nextKey];
          }
        }
      }
      return to;
    }
  });
}

// https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray
if (!Array.isArray) {
  Array.isArray = function(arg) {
    return Object.prototype.toString.call(arg) === '[object Array]';
  };
}

/**
 * This polyfill is for the CustomEvent()
 * constructor functionality in Internet Explorer 9 and higher
 * */
(function () {

  if ( typeof window.CustomEvent === "function" ) return false;

  function CustomEvent ( event, params ) {
    params = params || { bubbles: false, cancelable: false, detail: undefined };
    var evt = document.createEvent( 'CustomEvent' );
    evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
    return evt;
  }

  CustomEvent.prototype = window.Event.prototype;

  window.CustomEvent = CustomEvent;
})();

/**
 * Polyfill for Array.prototype.find
 * */
(function(){
  if (!Array.prototype.find) {
    Array.prototype.find = function(predicate) {
      if (this == null) {
        throw new TypeError('Array.prototype.find called on null or undefined');
      }
      if (typeof predicate !== 'function') {
        throw new TypeError('predicate must be a function');
      }
      var list = Object(this);
      var length = list.length >>> 0;
      var thisArg = arguments[1];
      var value;

      for (var i = 0; i < length; i++) {
        value = list[i];
        if (predicate.call(thisArg, value, i, list)) {
          return value;
        }
      }
      return undefined;
    };
  }
})();

/**
 * Polyfill for Object.keys
 */
(function() {
  // From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
  if (!Object.keys) {
    Object.keys = (function() {
      'use strict';
      var hasOwnProperty = Object.prototype.hasOwnProperty,
        hasDontEnumBug = !({ toString: null }).propertyIsEnumerable('toString'),
        dontEnums = [
          'toString',
          'toLocaleString',
          'valueOf',
          'hasOwnProperty',
          'isPrototypeOf',
          'propertyIsEnumerable',
          'constructor'
        ],
        dontEnumsLength = dontEnums.length;

      return function(obj) {
        if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) {
          throw new TypeError('Object.keys called on non-object');
        }

        var result = [], prop, i;

        for (prop in obj) {
          if (hasOwnProperty.call(obj, prop)) {
            result.push(prop);
          }
        }

        if (hasDontEnumBug) {
          for (i = 0; i < dontEnumsLength; i++) {
            if (hasOwnProperty.call(obj, dontEnums[i])) {
              result.push(dontEnums[i]);
            }
          }
        }
        return result;
      };
    }());
  }
})();

/**
 * Polyfill for Array.prototype.reduce
 */
(function() {
  // Algorithm steps ECMA-262, 5th edition, 15.4.4.21
  // http://es5.github.io/#x15.4.4.21
  if (!Array.prototype.reduce) {
    Array.prototype.reduce = function(callback/*, initialValue*/) {
      'use strict';
      if (this == null) {
        throw new TypeError('Array.prototype.reduce called on null or undefined');
      }
      if (typeof callback !== 'function') {
        throw new TypeError(callback + ' is not a function');
      }
      var t = Object(this), len = t.length >>> 0, k = 0, value;
      if (arguments.length >= 2) {
        value = arguments[1];
      } else {
        while (k < len && ! (k in t)) {
          k++;
        }
        if (k >= len) {
          throw new TypeError('Reduce of empty array with no initial value');
        }
        value = t[k++];
      }
      for (; k < len; k++) {
        if (k in t) {
          value = callback(value, t[k], k, t);
        }
      }
      return value;
    };
  }
})();

/**
 * Polyfill for Array.prototype.fill
 */
if (!Array.prototype.fill) {
  Array.prototype.fill = function(value) {

    if (this == null) {
      throw new TypeError('this is null or not defined');
    }

    var O = Object(this);

    var len = O.length >>> 0;

    var start = arguments[1];
    var relativeStart = start >> 0;

    var k = relativeStart < 0 ?
      Math.max(len + relativeStart, 0) :
      Math.min(relativeStart, len);

    var end = arguments[2];
    var relativeEnd = end === undefined ?
      len : end >> 0;

    var final = relativeEnd < 0 ?
      Math.max(len + relativeEnd, 0) :
      Math.min(relativeEnd, len);

    while (k < final) {
      O[k] = value;
      k++;
    }

    return O;
  };
}

// https://github.com/es-shims/es5-shim/blob/master/es5-sham.js
if (!Object.freeze) {
  Object.freeze = function freeze(object) {
    if (Object(object) !== object) {
      throw new TypeError('Object.freeze can only be called on Objects.');
    }
    // this is misleading and breaks feature-detection, but
    // allows "securable" code to "gracefully" degrade to working
    // but insecure code.
    return object;
  };
}

/**
 * Polyfill for Object.values
 */
(function() {
  if (!Object.values) {
    Object.values = function (obj) {
      return Object
        .keys(obj)
        .map(function (key) {
          if (obj.hasOwnProperty(key)) {
            return obj[key];
          }
        });
    }
  }
})();

(function($){var g,d,j=1,a,b=this,f=!1,h="postMessage",e="addEventListener",c,i=b[h];$[h]=function(k,l,m){if(!l){return}k=typeof k==="string"?k:$.param(k);m=m||parent;if(i){m[h](k,l.replace(/([^:]+:\/\/[^\/]+).*/,"$1"))}else{if(l){m.location=l.replace(/#.*$/,"")+"#"+(+new Date)+(j++)+"&"+k}}};$.receiveMessage=c=function(l,m,k){if(i){if(l){a&&c();a=function(n){if((typeof m==="string"&&n.origin!==m)||($.isFunction(m)&&m(n.origin)===f)){return f}l(n)}}if(b[e]){b[l?e:"removeEventListener"]("message",a,f)}else{b[l?"attachEvent":"detachEvent"]("onmessage",a)}}else{g&&clearInterval(g);g=null;if(l){k=typeof m==="number"?m:typeof k==="number"?k:100;g=setInterval(function(){var o=document.location.hash,n=/^#?\d+&/;if(o!==d&&n.test(o)){d=o;l({data:o.replace(n,"")})}},k)}}}})(jQuery);

//author: sparrow.jang
//verion: 1.0.1
!function(){"use strict";var a="placeholder"in document.createElement("input"),t=angular.module("html5.placeholder",[]),n=function(a,t){var n=a.getAttributeNode(t);return n?n.nodeValue:n};t.factory("placeholder",function(){var t;if(a)t=function(a,t){t&&t({back:function(){}})};else{var e="placeholderTmp"+ +new Date,r=function(a,t){return angular.forEach(t,function(t){a.push(t)}),a},o={commit:function(a){angular.forEach(a,function(a){var t,r=angular.element(a);t=n(a,"placeholder"),r.val()==t&&(r.data(e,r.val()),r.val(""))})},doRollback:function(a){angular.forEach(a,function(a){var t,n=angular.element(a);t=n.data(e),t&&(n.val(t),n.data(e,null))})}};t=function(a,t){var n;a.length&&"form"==a[0].tagName.toLowerCase()?(n=a.find("input"),n=r(a.find("textarea"),n)):n=a,o.commit(n),t&&t({back:function(){o.doRollback(n)}})}}return{ensure:t}}),a||t.directive("placeholder",[function(){var a,t,e,r,o=+new Date,l="_placeholder_"+o,u="focus",c="blur",i=/msie 9/i.test(navigator.userAgent);return e=function(a){"password"==a.data(l).type&&a.attr("type","text")},r=function(a){"password"==a.data(l).type&&a.attr("type","password")},a=function(){var a=angular.element(this);a.val()==n(this,"placeholder")&&(a.val(""),r(a))},t=function(){var a=angular.element(this);""==a.val()&&(a.val(n(this,"placeholder")),e(a))},{link:function(n,o,d){n.$watch("ready",function(){return"password"!=o.attr("type")||i?(o.val(d.placeholder).data(l,{type:(o.attr("type")||"").toLowerCase()}).bind(u,a).bind(c,t),e(o),void n.$on("$destroy",function(){o.unbind(u,a).unbind(c,t),r(o)})):{}})}}}])}();
angular.module('tp.i18n',['ajaxServices', 'ngCookies', 'tmh.dynamicLocale', 'tinypassServices'])
    .config(['tmhDynamicLocaleProvider', function (tmhDynamicLocaleProvider) {
        // we should keep these locale's coming from cloudflare like this
        tmhDynamicLocaleProvider.localeLocationPattern('/libs/angular-1.2.22/i18n/angular-locale_{{locale | replaceUnderscoreToDash}}.js');
    }])
    .run(['lang', function(lang) {
      var initLocale = (TPParam.TRANSLATION_CONFIG && TPParam.TRANSLATION_CONFIG.initialLocaleId) || 'en_US';

      lang.update(initLocale);
    }])
    .directive('autoFocus', function($timeout) {
      return {
        restrict: 'AC',
        link: function(_scope, _element) {
          var element = _element[0];
          $timeout(function(){
            var value = element.value;
            element.value = '';
            element.focus();
            element.value = value;
          }, 100);
        }
      };
    })
    .directive('t', ['$compile', 'lang', 'getTextFromLocales', function ($compile, lang, getTextFromLocales) {
        // <t arg1="time" arg2="date">current time: {1}, date: {2}</t>
        var attrRegex = /^arg(\d+)$/i;
        return {
            restrict: 'AE',
            compile: function (tElement, tAttr) {
                var text = tElement.html();
                var args = [];
                angular.forEach(tAttr, function(value, key) {
                    var match = key.match(attrRegex);
                    if (match && match[1]) {
                        args[match[1]] = value;
                    }
                });
                return postLink;

                function postLink(scope, el, attr) {
                    var templateContext = lang.getTemplateContext();
                    var context = attr.context ? attr.context : templateContext;

                    onChangeLanguage();

                    activate();

                    function activate() {
                        lang.on(onChangeLanguage);
                        scope.$on('$destroy', function() {
                            lang.off(onChangeLanguage);
                        });
                    }

                    function onChangeLanguage(locale, systemDefaultLocale, locales) {
                      if (!locale) {
                        return;
                      }

                      var getText = getTextFromLocales(locale, systemDefaultLocale, locales);

                      el.empty().append(lang.formatString(getText(context, text), args));
                      $compile(el.contents())(scope);
                    }
                }
            }
        };
    }])
    .directive('languageSelector', ['lang', function (lang) {
        return {
            restrict: 'E',
            scope: {},
            template:
            '<div ng-show="areLanguagesAvailable()" class="language-selector">'+
            '   <div id="language-label" class="language-selector__label" ng-click="toggleMenu()">{{locale | shortLocale}}</div>'+
            '   <ul id="language-list" class="language-selector__list" ng-class="{\'show\': opened === true, \'hide\': opened === false}">'+
            '       <li class="language-selector__list-item" ng-repeat="(index, value) in languages | filter:showAvailableLanguage" ng-click="changeLanguage(value.locale)">'+
            '           <div class="language-selector__country" ng-class="{\'selected\': value.locale === locale}">{{value.localized}}' +
            '             <span class="language-selector__country-image language-selector__country-image--{{value.locale | getCountryCode}}"></span>'+
            '           </div>'+
            '       </li>'+
            '   </ul>'+
            '</div>',
            link: link,
            controller: ['$scope', ctrl]
        };

        function link(scope, elem) {
            scope.toggleMenu = toggleMenu;
            scope.opened = false;
            function toggleMenu() {
                scope.opened = !scope.opened;
                jQuery(window).off('click');

                jQuery(window).on('click', closeMenu);
            }

            function closeMenu(e) {
                if (jQuery(e.target)[0] !== elem.find('#language-label')[0]) {
                    scope.opened = false;
                    jQuery(window).off('click');
                    scope.$apply();
                }
            }
        }

        function ctrl($scope) {
            $scope.languages = [];
            $scope.locale = null;
            $scope.showAvailableLanguage = showAvailableLanguage;
            $scope.changeLanguage = changeLanguage;
            $scope.isLanguagesEnabled = TPParam.TRANSLATION_CONFIG.isEnabled;

            activate();

            $scope.areLanguagesAvailable = function () {
                return $scope.isLanguagesEnabled && $scope.languages.length > 1;
            };

            function activate() {
                lang.on(onChangeLanguage);
                $scope.$on('$destroy', function() {
                    lang.off(onChangeLanguage);
                });

                lang.list()
                    .then(onLoadLanguages);
            }

            function onChangeLanguage(locale) {
                $scope.locale = locale;
            }

            function onLoadLanguages(languages) {
                $scope.languages = languages.sort(function(a, b) {
                  if (a.localized.toLowerCase() === b.localized.toLowerCase()) return 0;
                  return a.localized.toLowerCase() > b.localized.toLowerCase() ? 1 : -1;
                });
            }

            function showAvailableLanguage(language) {
                return language.isEnabled ? language : false;
            }

            function changeLanguage(locale) {
              $scope.locale = locale;
              lang.update($scope.locale);
            }
        }
    }])
    .filter('shortLocale', function() {
        return function(text) {
            var splittedText = text.toUpperCase().split('_');
            return splittedText[0] + '-' + splittedText[1];
        }
    })
    .filter('t', ['lang', function(lang) {
        return function(text, args) {
            var templateContext = lang.getTemplateContext();

            return lang.trc(templateContext, text, args);
        }
    }])
    .filter('tc', ['lang', function(lang) {
        return function(text, context, args) {
            return lang.trc(context, text, args);
        }
    }])
    .filter('replaceUnderscoreToDash', function() {
        return function(text){
            return text.toString().toLowerCase().replace('_','-');
        }
    })
    .filter('replaceDashToUnderscore', function() {
        return function(text) {
            var localeArr;
            if (text.match(/\-/g) === null) {
                return text;
            }

            localeArr = text.toUpperCase().split('-');
            localeArr[0] = localeArr[0].toLowerCase();
            return localeArr.join('_');
        }
    })
    .filter('getCountryCode', function() {
      return function(text) {
        return text.match(/\w{2}$/)[0].toLowerCase();
      }
    })
  .factory('getTextFromLocales', [function () {
    return function (locale, systemDefaultLocale, locales) {
      return function (context, text) {
        context = context ? context + '\u0004' : '';

        var key = context + text;

        var systemDefaultText;
        var systemDefaultTexts = locales && locales[systemDefaultLocale];

        var preferredLocaleTexts = locales && locales[locale];
        var preferredLocaleText = preferredLocaleTexts && (preferredLocaleTexts[key] || preferredLocaleTexts[text]);

        //If there is no texts for preferred(selected/current) locale
        if (typeof preferredLocaleText === 'undefined') {
          systemDefaultText = systemDefaultTexts && (systemDefaultTexts[key] || systemDefaultTexts[text]);
        }
        //If there are no texts for preferred(selected/current) locale and default locale
        return preferredLocaleText || systemDefaultText || text;
      };
    };
  }])
    .factory('lang', ['tpHTTP', '$q', '$locale', '$cookies', 'tmhDynamicLocale',
      'resolveLocale', 'cookieLang', '$rootScope', '$filter', 'getTextFromLocales', function (
        tpHTTP,
        $q,
        $locale,
        $cookies,
        tmhDynamicLocale,
        resolveLocale,
        cookieLang,
        $rootScope,
        $filter,
        getTextFromLocales
    ) {
        var locale = resolveLocale();
        var localeChanged = false;
        var sundayFirstWeekDayByCountry = ['en_US', 'en_GB', 'pt_PT', 'es_CO', 'pt_BR', 'fr_CA', 'ja_JP', 'zz_ZZ'];

        var handlers = [];
        var languages = null;
        var loadLanguages = null;
        var systemDefaultLocale = (TPParam.TRANSLATION_CONFIG && TPParam.TRANSLATION_CONFIG.systemDefaultLocale) || 'en_US';

        var translationMaps = {
                translationMapStatic: {},
                translationMapDynamic: {}
            };

        var locales = {};
        var cachedDynamicLocales = {};
        var _timer;

        init();

        return {
            update: update,
            on: on,
            off: off,
            tr: tr,
            trc: trc,
            list: list,
            formatString: formatString,
            getLang: getLang,
            getTemplateContext: getTemplateContext,
            getCorrectStartWeekDay: getCorrectStartWeekDay,
            getLanguageTag: getLanguageTag
        };

        function getLang() {
            return localeChanged && locale || '';
        }

        function getTemplateContext() {
          return (window.TPParam.TRANSLATION_CONFIG && window.TPParam.TRANSLATION_CONFIG.templateContext) || ''
        }

        function getLanguageTag() {
            // returns IETF language tag, e.g. 'en-US'
            var lang = getLang() || locale;
            return lang.replace("_", "-");
        }

        function init() {
            $rootScope.$on('CHECK_UPDATED_LANGUAGE', checkLangChange);
            startLangChangedWatcher();
            updateMomentJsLocale();
        }

        function startLangChangedWatcher() {
            stopLangChangedWatcher();
            _timer = setInterval(checkLangChange, 500);
        }

        function stopLangChangedWatcher() {
            if (_timer) {
                clearInterval(_timer);
            }
        }

        function checkLangChange() {
            var langInCookie = cookieLang.getCookie('LANG_CHANGED');
            if (langInCookie && langInCookie !== locale && langInCookie !== "\"\"") {
                update(langInCookie);
            }
        }

        function list() {
          if (!TPParam.TRANSLATION_CONFIG.isEnabled) {
            return $q.when([]);
          }

          if (!loadLanguages) {
            loadLanguages = $q.when(TPParam.TRANSLATION_CONFIG.languages).then(function (data) {
              languages = data;
              update(locale);
              return data;
            });
          }

          return loadLanguages;
        }

        function getCorrectStartWeekDay() {
          var lang = getLang() || locale;

          return sundayFirstWeekDayByCountry.indexOf(lang) === -1;
        }

        function tr(text) {
            var args = new Array(arguments.length - 1);
            for(var i = 0; i < args.length; ++i) {
                args[i] = arguments[i + 1];
            }
            var getText = getTextFromLocales(locale, systemDefaultLocale, locales);

            return formatString(getText(null, text), args);
        }

        // now we need context variable for gradle to put key in certain context
        function trc(context, text, params) {
            var args;
            if (angular.isArray(params)) {
                args = params;
            } else {
                args = new Array(arguments.length - 2);
                for(var i = 0; i < args.length; ++i) {
                    args[i] = arguments[i + 2];
                }
            }

          var getText = getTextFromLocales(locale, systemDefaultLocale, locales);

          return formatString(getText(context, text), args);
        }

        function formatString(text, args) {
            if (angular.isUndefined(text) || text === null) {
                return text;
            }
            return text.replace(/<t([^>]+)?>|<\/t>/g, '').replace(/{(\d+)}/g, function (match, number) {
                return typeof args[number] !== 'undefined' ? args[number] : match;
            });
        }

        function handle(texts) {
          locales[locale] = texts;

          handlers.forEach(function (handler) {
            handler(locale, systemDefaultLocale, locales);
          });
        }

        function loadTranslationMap(url, locale) {
            var deferred = $q.defer();
            var script;
            var heads;
            var head;

            if (url) {
                script = document.querySelector('script[src*="' + url + '"]');
                if (!script) {
                    heads = document.getElementsByTagName('head');
                    if (heads && heads.length) {
                        head = heads[0];
                        if (head) {
                            script = document.createElement('script');
                            script.onload = function() {
                                if(!window['translation' + locale]) {
                                    return deferred.reject('Unable to load');
                                }
                                return deferred.resolve(window['translation' + locale]);
                            };

                            script.setAttribute('src', url);
                            script.setAttribute('type', 'text/javascript');
                            script.setAttribute('charset', 'utf-8');
                            head.appendChild(script);
                        }
                    }
                }
            }
            return deferred.promise;
        }

        function loadLocale(localeId) {
            var translationStaticPath = '/ng/common/i18n/platform-translation-map_' + localeId + '.js?version=' + TPParam.TRANSLATION_CONFIG.version;
            var translationDynamicPath = TPParam.TRANSLATION_CONFIG.loadTranslationUrl + '&language=' + localeId;
            var staticTranslation = window['translation_static_' + localeId];

            if (TPParam.TRANSLATION_CONFIG.isStatic) {
                if (staticTranslation) {
                    return $q.when(staticTranslation);
                }

                return loadTranslationMap(translationStaticPath, '_static_' + localeId).then(function() {
                    return $q.when(window['translation_static_' + localeId]);
                });
            }

            return $q.all({
                translationMapStatic: loadTranslationMap(translationStaticPath, '_static_' + localeId).then(function(staticResult) {
                    return staticResult;
                }),
                translationMapDynamic: loadTranslationMap(translationDynamicPath, '_dynamic_' + localeId).then(function(dynamicResult) {
                    return dynamicResult;
                })
            }).then(function(res) {
                translationMaps.translationMapStatic  = res.translationMapStatic;
                translationMaps.translationMapDynamic = res.translationMapDynamic;

                return $q.when(angular.extend({}, res.translationMapStatic, res.translationMapDynamic));
            }).catch(function(res) {
              locales = omit(locales, localeId);
              // TODO: Handle error in a proper way. We can't use generateErrorMessage for that purpose
              /* return TinypassService.generateErrorMessage(res); */
            });
        }

        function omit(object, fields) {
            var fieldsToOmit = [];
            var objectToParse = {};

            if (typeof fields === 'string') {
                fieldsToOmit = [fields];
            }

            if (angular.isArray(fields)) {
                fieldsToOmit = [].concat(fields);
            }

            if (angular.isArray(object)) {
                object.forEach(function(value, index) {
                    objectToParse[index] = value;
                });
            }

            if(angular.isObject(object) && !angular.isArray(object)) {
                objectToParse = angular.extend({}, object);
            }

            fieldsToOmit.forEach(function(field) {
                delete objectToParse[field];
            });

            return objectToParse;
        }

        function on(fn, notRunImmediately) {
            handlers.push(fn);

            if (!notRunImmediately) {
                fn(locale, systemDefaultLocale, locales);
            }
        }

        function off(fn) {
            var index = handlers.indexOf(fn);
            if (index !== -1) {
                handlers.splice(index, 1);
            }
        }

        function update(newLocale) {
          stopLangChangedWatcher();

          if (!(TPParam.TRANSLATION_CONFIG && TPParam.TRANSLATION_CONFIG.isEnabled)) {
            cookieLang.removeCookie('LANG');
            cookieLang.removeCookie('LANG_CHANGED');
            return;
          }

          if (angular.isUndefined(newLocale)) {
            newLocale = cookieLang.getCookie('LANG') || systemDefaultLocale;
          }

          var isLocaleListed = languages && languages.some(function (lang) {
            return lang.locale === newLocale; });

          if (!languages || isLocaleListed) {
            setLngCookie(newLocale);
            updateMomentJsLocale();

            loadLocales(newLocale).then(handle).then(function () {
              $rootScope.$broadcast('EVENT_I18N_LOCALE_CHANGED');
            });
          }

          startLangChangedWatcher();
        }

        function loadLocales(newLocale) {
          if (typeof locales[systemDefaultLocale] === 'undefined') {
            return $q.all(getLocale(newLocale), getLocale(systemDefaultLocale));
          }

          return getLocale(newLocale);
        }

        function getLocale(newLocale) {
            var translationStatic  = window['translation_static_' + newLocale];
            var translationDynamic = window['translation_dynamic_' + newLocale];

            if (locales[newLocale]) {
                return $q.when(locales[newLocale]);
            }

            if (translationStatic || translationDynamic) {
                locales[newLocale] = angular.extend({}, translationStatic, translationDynamic);
                return $q.when(locales[newLocale]);
            }

            return loadLocale(newLocale)
                .then(function (texts) {
                    locales[newLocale] = texts;
                    return texts;
                });
        }

        function setLngCookie(newLocale) {
            locale = newLocale;
            localeChanged = true;

            cookieLang.setCookie('LANG', locale, 1500);
            cookieLang.setCookie('LANG_CHANGED', locale, 1);

            var newLocaleId = $filter('replaceUnderscoreToDash')(newLocale);
            if(!cachedDynamicLocales[newLocaleId] && ($locale.id === newLocaleId)) {
                cachedDynamicLocales[newLocaleId] = angular.copy($locale);
            }

            if(cachedDynamicLocales[newLocaleId]) {
                overrideValues($locale, cachedDynamicLocales[newLocaleId]);
            }

            //When we load page first time, initial angular locale file must be loaded(For example: 'angular-locale_ru-ru') on the back-end
            //No need to load on the front-end dynamically.
            if (newLocale !== TPParam.TRANSLATION_CONFIG.initialLocaleId) {
                if(!cachedDynamicLocales[newLocaleId]) {
                    tmhDynamicLocale.set(newLocaleId).then(function(locale){
                        cachedDynamicLocales[locale.id] = angular.copy(locale);
                    });
                }

                updateInitialLocaleId(locale);
            }
        }

        function updateInitialLocaleId(newLocaleId) {
            if (window.TPParam.TRANSLATION_CONFIG && window.TPParam.TRANSLATION_CONFIG.initialLocaleId) {
                window.TPParam.TRANSLATION_CONFIG.initialLocaleId = newLocaleId;
            }
        }

        function updateMomentJsLocale() {
            if (typeof(moment) !== 'undefined') {
                try {
                    var language = getLanguageTag();

                    switch (language) {
                        case 'zz-ZZ':
                          language = 'x-pseudo';
                          break;
                        case 'sr-RS':
                          language = 'sr-cyrl';
                          break;
                    }

                    if (moment.locale) {
                        // 2.8.1+
                        moment.locale(language);
                    } else {
                        moment.lang(language);
                    }
                } catch (e) {}
            }
        }

        function overrideValues(oldObject, newObject) {
            angular.forEach(oldObject, function(value, key) {
                if (!newObject[key]) {
                    delete oldObject[key];
                } else if (angular.isArray(newObject[key])) {
                    oldObject[key].length = newObject[key].length;
                }
            });
            angular.forEach(newObject, function(value, key) {
                if (angular.isArray(newObject[key]) || angular.isObject(newObject[key])) {
                    if (!oldObject[key]) {
                        oldObject[key] = angular.isArray(newObject[key]) ? [] : {};
                    }
                    overrideValues(oldObject[key], newObject[key]);
                } else {
                    oldObject[key] = newObject[key];
                }
            });
        }
    }])
    .factory('resolveLocale', ['$cookies', '$locale', 'tmhDynamicLocale', '$filter', 'cookieLang',
      function($cookies, $locale, tmhDynamicLocale, $filter, cookieLang) {
        return function resolveLocale() {
            var cookieLocale = cookieLang.getCookie('LANG');

            //var locale = $filter('replaceDashToUnderscore')(cookieLocale || $locale.id);

            //tmhDynamicLocale.set($filter('replaceUnderscoreToDash')(locale));

            return "en_US";
        }
    }])
    .factory('cookieLang', ['TinypassService', function(tinypassService) {
        //Factory uses in checkout/resource/widget/checkout/1.0/js/paypalbtModule.js
        return {
            getCookie: getCookie,
            setCookie: setCookie,
            removeCookie: removeCookie,

            getCountryCode: getCountryCode,
            getLanguageCode: getLanguageCode
        };

        function getCountryCode() {
            if (getCookie("LANG") === null || getCookie("LANG") === undefined || getCookie("LANG") === '') {
                return "us";
            }
            return getCookie("LANG").split("_")[1].toLowerCase();
        }

        function getLanguageCode() {
            if (getCookie("LANG") === null || getCookie("LANG") === undefined || getCookie("LANG") === '') {
                return "en";
            }
            return getCookie("LANG").split("_")[0];
        }

        function getCookie(name) {
            var value = '; ' + document.cookie;
            var parts = value.split('; ' + name + '=');
            if (parts.length > 1) return parts.pop().split(';').shift();
        }

        function setCookie(cname, cvalue, exdays) {
            var d = new Date();
            var expires;
            d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
            expires = 'expires=' + d.toUTCString();
            document.cookie = cname + '=' + cvalue + '; ' + expires + '; domain=' + tinypassService.getTopDomain() + '; path=/;' + tinypassService.getCookieSameSiteAndSecureAttrString();
        }

        function removeCookie(cname) {
            var expires = 'expires=' + (new Date('February 02, 1972 02:02:02')).toUTCString();
            document.cookie = cname + '=' + '; ' + expires + '; domain=' + tinypassService.getTopDomain() + '; path=/;' + tinypassService.getCookieSameSiteAndSecureAttrString();
        }
    }])
    .factory('updateDirAttrOnLangChanges', [
      '$document',
      'lang',
      'ngEventService',
      function(
        $document,
        lang,
        ngEventService
      ) {
        var RTL_LOCALES = ['he_IL', 'ar_EG'];
        var DIRECTIONS = {
          RIGHT_TO_LEFT: 'rtl',
          LEFT_TO_RIGHT: 'ltr',
        }

        function setDirAttribute(direction) {
          $document[0].documentElement.setAttribute('dir', direction);
        }

        ngEventService.subscribe('langChanged', function (event, params) {
          if (params.lang) {
            lang.update(params.lang);
          }
        });

        return function(onLangChange) {
          var currentLocale;
          var currentDirection;

          lang.on(function(newLocale) {
            if (newLocale === currentLocale) {
              return;
            }

            var newLocaleDirection = RTL_LOCALES.indexOf(newLocale) !== -1 ? DIRECTIONS.RIGHT_TO_LEFT : DIRECTIONS.LEFT_TO_RIGHT;

            if (newLocaleDirection !== currentDirection) {
              currentDirection = newLocaleDirection;
              setDirAttribute(newLocaleDirection);
            }

            currentLocale = newLocale;

            if (onLangChange) {
              onLangChange(newLocale);
            }
          });
        }
    }]);

(function () {
  var dateServices = angular.module('dateServices', []);
  var withoutSpinnerHeaderName = 'Piano-request-without-spinner';

  function isIncludesWithoutSpinnerHeader(response) {
    var headers = response && response.config && response.config.headers || {};
    return !!headers[withoutSpinnerHeaderName];
  }

  dateServices.factory('fromIsoToJsDate', function () {
    return function (field, obj) {
      if (!obj.length) {
        if (obj[field]) {
          obj[field] = moment(obj[field]).format();
        }
        return obj[field];
      } else {
        for (var i = obj.length - 1; i >= 0; i--) {
          if (obj[i][field]) {
            obj[i][field] = moment(obj[i][field]).format();
          }
        }
        return obj;
      }
    };
  });

  dateServices.factory('timeStampToDate', function () {
    return function(timestamp) {
      if (!timestamp) {
        return timestamp;
      }
      if (timestamp && (timestamp < 10000000000)) {
        return new Date(timestamp * 1000);
      } else {
        return new Date(timestamp);
      }
    };
  });

  dateServices.factory('TimezoneService', function () {
    return {
      /**
       * Backend expects dates in requests to be in this format (ISO with offset without milliseconds).
       */
      SEND_DATETIME_FORMAT: 'YYYY-MM-DDTHH:mm:ssZZ',

      /**
       * Given date, representing some moment, returns Moment, which points to the same moment,
       * but with the offset based on `TPParam.CLIENT_TIMEZONE`. So the input and output point to the same
       * moment on the timeline (unlike in {@link dateToClientTimezoneMoment}).
       * If no date provided, returns a Moment, that points to the current moment with the above said offset.
       * @throws Would throw an error if `moment.js` or `moment-timezone.js` are not available.
       * @param {Date | string | object} [date] - Any valid representative of MomentInput interface.
       * @returns {Moment}
       */
      dateToTimezonedMoment: function dateToTimezonedMoment(date) {
        return moment.tz(date, TPParam.CLIENT_TIMEZONE);
      },

      /**
       * Given Date object, representing some moment, returns Moment, which has the same date and time,
       * but in the `TPParam.CLIENT_TIMEZONE` timezone. So the input and output point to the two different
       * moments on the timeline (unlike in {@link dateToTimezonedMoment}).
       * @example
       * var date = new Date(); // "Thu Mar 21 2019 10:33:21 GMT+0400 (Samara Standard Time)"
       * TPParam.CLIENT_TIMEZONE; // "America/New_York"
       * dateToClientTimezoneMoment(date).format(); // "2019-03-21T10:33:21-04:00"
       * @throws Would throw an error if `moment.js` or `moment-timezone.js` are not available.
       * @param {Date} date
       * @returns {Moment}
       */
      dateToClientTimezoneMoment: function dateToClientTimezoneMoment(date) {
        return moment.tz({
          year: date.getFullYear(),
          month: date.getMonth(),
          date: date.getDate(),
          hour: date.getHours(),
          minute: date.getMinutes(),
          second: date.getSeconds(),
          millisecond: date.getMilliseconds(),
        }, TPParam.CLIENT_TIMEZONE);
      },

      /**
       * @typedef {Object} DateRangeStringObject
       * @property {string} from
       * @property {string} to
       */

      /**
       * Given two Date objects (range start and range end) returns an object with strings,
       * which are the provided Dates with justified timezones and times in {@link SEND_DATETIME_FORMAT}.
       * Only the date of the Date objects is considered: offset is reset (see {@link dateToClientTimezoneMoment})
       * and time is set to 00:00:00 for `from` and 23:59:59 for `to`.
       * The main use case is when the pn-datepicker provides us the date range, and we need these dates NOT in local timezone.
       * @param {Date} from
       * @param {Date} to
       * @returns {DateRangeStringObject}
       */
      datesToTimezonedStringsRange: function datesToTimezonedStringsRange(from, to) {
        return {
          from: this.dateToClientTimezoneMoment(from).startOf('day').format(this.SEND_DATETIME_FORMAT),
          to: this.dateToClientTimezoneMoment(to).endOf('day').format(this.SEND_DATETIME_FORMAT),
        };
      },

      /**
       * Given Moment, which points to some moment on the timeline, returns Date object, which has
       * the same date and time, but in the browser's timezone.
       * The returned Date points to another moment on the timeline, so we cannot use {@link Moment.toDate}.
       * @param {Moment} m
       * @returns {Date}
       */
      momentToDate: function momentToDate(m) {
        return new Date(
          m.year(),
          m.month(),
          m.date(),
          m.hour(),
          m.minute(),
          m.second()
        );
      },

      dateInClientTimezoneToBrowserTimezone: function dateInClientTimezoneToBrowserTimezone(date) {
        var m = moment.tz(date, TPParam.CLIENT_TIMEZONE);
        return new Date(
          m.year(),
          m.month(),
          m.date(),
          m.hour(),
          m.minute(),
          m.second()
        );
      }
    };
  });

  dateServices.factory('ISODatetimeService', function () {
    return {
      ISO8601_DATE_FORMAT: 'YYYY-MM-DD',
      ISO8601_DATETIME_FORMAT: 'YYYY-MM-DD HH:MM',

      dateToString: function dateToString(value) {
        if (moment.tz) {
          return moment.tz(value, TPParam.CLIENT_TIMEZONE).format(this.ISO8601_DATE_FORMAT);
        } else {
          return moment(value).format(this.ISO8601_DATE_FORMAT);
        }
      },

      datetimeToString: function datetimeToString(value) {
        if (moment.tz) {
          return moment.tz(value, TPParam.CLIENT_TIMEZONE).format(this.ISO8601_DATETIME_FORMAT);
        } else {
          return moment(value).format(this.ISO8601_DATETIME_FORMAT);
        }
      }
    };
  });

  var ajaxServices = angular.module('ajaxServices', []);

  ajaxServices.config(['$httpProvider', function ($httpProvider) {
    $httpProvider.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest";
    $httpProvider.defaults.headers.common["Ng-Request"] = "1";
    $httpProvider.interceptors.push('tpHttpInterceptor');
    var spinnerFunction = function (data, headersGetter) {
      var headers = headersGetter && headersGetter();
      var requestWithoutSpinnerHeader = headers && headers[withoutSpinnerHeaderName];

      if (requestWithoutSpinnerHeader || tinypass.withoutWaitPanel) {
        return data;
      }

      tinypass.showWaitPanel();
      return data;
    };
    $httpProvider.defaults.transformRequest.push(spinnerFunction);
  }]);
  ajaxServices.factory('tpHttpInterceptor', ['$q', function ($q) {
    return {
      'response': function (response) {
        if (response.data) {
          if (response.data.models) {
            response.models = response.data.models;
          }
          if (response.data.errors) {
            response.errors = response.data.errors;
          }
        }
        tinypass.updateMetaRefresh();

        if (!isIncludesWithoutSpinnerHeader(response)) {
          tinypass.hideWaitPanel();
        }

        return response;
      },
      'responseError': function (rejection) {
        if (rejection.data) {
          if (rejection.data.models) {
            rejection.models = rejection.data.models;
          }
          if (rejection.data.errors) {
            rejection.errors = rejection.data.errors;
          }
        }

        if (!isIncludesWithoutSpinnerHeader(rejection)) {
          tinypass.hideWaitPanel();
        }

        return $q.reject(rejection);
      }
    }
  }]);

  ajaxServices.factory('repeatHTTP', function($q) {
    var repeat = function (httpCall, config) {
      var config = config || {};
      var defer = config.defer;
      var attempt = config.attempt || 0;
      var times = config.times || 1;
      var cancelConfig = config.cancelConfig || {};

      if (!defer) {
        Log.info('defer is required');
        return $q.reject();
      }

      httpCall()
        .then(function (response) {
          defer.resolve(response);
        })
        .catch(function (response) {
          if (cancelConfig.cancel) {
            return;
          }

          if (attempt >= times) {
            defer.reject(response);
          } else {
            repeat(httpCall, Object.assign({}, config, { attempt: attempt + 1 }));
          }
        });
        return defer.promise;
    };
    return repeat;
  });

  ajaxServices.factory('tpHTTP', ['$http', '$window', '$q', '$rootScope',
    function ($http, $window, $q, $rootScope) {
      var f = function tpHTTP(config, isAbsoluteUrl) {
        var isRelativeUrl = !isAbsoluteUrl;

        if (isRelativeUrl) {
          config.url = $window.TPConfig.PATH + config.url;
        }

        var promise = $http(config);
        promise.then(f.processHeaders);
        return promise;
      };
      f.get = function (url, config) {
        url = $window.TPConfig.PATH + url;
        var promise = $http.get(url, config);
        promise.then(f.processHeaders);
        return promise;
      };
      f.post = function (url, data, config) {
        url = $window.TPConfig.PATH + url;
        var promise = $http.post(url, data, config);
        promise.then(f.processHeaders);
        return promise;
      };
      f.jsonp = function (url, params) {
        params = params || {};
        params.callback = params.callback || 'JSON_CALLBACK';

        var promise = $http.jsonp(url, {
          params: params
        });

        return promise.then(function (response) {
          return response.data;
        });
      };
      f.processResponse = function (response) {
        if (response.data && response.data.models) {
          response.models = response.data.models;
        }
        if (response.data && response.data.errors) {
          response.errors = response.data.errors;
        }

        return response;
      };
      f.processResponseCatch = function (response) {
        if (response.data && response.data.models) {
          response.models = response.data.models;
        }
        if (response.data && response.data.errors) {
          response.errors = response.data.errors;
        }
        return $q.reject(response);
      };

      f.processHeaders = function (response) {
        var headers = response.headers();
        if (headers['execute_js_after']) {
          try {
            eval(headers['execute_js_after']);
          } catch (ex) {
          }
        }
        return response;
      };

      /*
       Extract errors from an angular response message
       */
      f.extractErrors = function (jsonData, scopeForm, scope) {
        if (jsonData && (jsonData['errors'] || jsonData['modalErrors'])) {
          scope.formErrors = (jsonData['errors'] && jsonData['errors'].length) ? jsonData['errors'] : jsonData['modalErrors'];
          angular.forEach(scope.formErrors, function (error) {
            var field = error['field'];
            var key = error['key'];
            var msg = error['msg'];
            if (scopeForm && scopeForm[field]) {
              angular.element('#' + field).focus();
              scopeForm[field].$setValidity('error', false);
              scopeForm[field].$error['msg'] = msg;
            }
          });
        }

      };

      /*
       Reset the form errors.
       */
      f.resetFormErrors = function (scopeForm, scope, jsonData) {
        try {
          if (jsonData) {
            jsonData['errors'] = [];
            jsonData['modalErrors'] = [];
          }
          angular.forEach(scope.formErrors, function (error) {
            scopeForm[error['field']].$setValidity('error', true);
            scopeForm[error['field']].$setPristine();
          });
          delete scope.formErrors;
          scopeForm.$setPristine();
          if (!scope.$$phase) {
            scope.$apply();
          }
        } catch (ex) {
          Log.info("Could not reset form errors!", ex);
        }
      };

      f.setFieldsValid = function (scopeForm, scope) {
        try {
          angular.forEach(scopeForm, function (field, name) {
            if (name.indexOf('$') != 0) {
              field.$setValidity('error', true);
              field.$setPristine();
            }
          });

          scopeForm.$setPristine();
          if (!scope.$$phase) {
            scope.$apply();
          }
        } catch (ex) {
          Log.info("Could not reset form errors!", ex);
        }
      };

      f.extractModels = function (data, $scope) {
        if (data && data.models) {
          for (var name in data.models)
            $scope[name] = data.models[name];
        }
        if (data && data.routes) {
          if (typeof $scope.routes == 'undefined')
            $scope.routes = {};
          for (var route in data.routes)
            $scope.routes[route] = data.routes[route];
        }
      };

      f.promiseResponse = function (httpPromiser, scope) {
        var promiser = $q.defer();
        scope.errors = [];
        httpPromiser.then(function (rsp) {
          if (rsp && rsp["errors"]) {
            angular.copy(rsp["errors"], scope.errors);
          }

          if (rsp && rsp.models) {
            for (var name in rsp.models) {
              if (rsp.models.hasOwnProperty(name)) {
                scope[name] = rsp.models[name];
              }
            }
          }
          promiser.resolve();
        }).catch(function (rsp) {
          if (rsp && rsp["errors"]) {
            angular.copy(rsp["errors"], scope.errors);
          }
          promiser.reject();
        });
        return promiser.promise;
      };

      /**
       * binder for scope with extractModels
       * @param $scope
       * @returns {Function} binded function
       */
      f.extractModelsTo = function ($scope) {
        return function (rsp) {
          f.extractModels(rsp.data, $scope);
        };
      };

      /**
       * Success handler
       * @param loaded
       * @returns {Function}
       */
      f.successHander = function (loaded) {
        return function (rspData) {
          loaded.resolve({ data: rspData, ok: true });
        };
      };

      /**
       * Error handler
       * @param loaded
       * @returns {Function}
       */
      f.errorHander = function (loaded) {
        return function (rspData) {
          loaded.resolve({ data: rspData, ok: false });
        };
      };

      f.promiser = function (httpPromise) {
        var promiser = $q.defer();
        httpPromise.then(f.successHander(promiser))
          .catch(f.errorHander(promiser));
        return promiser.promise;
      };

      f.buildUrl = function (url, params) {
        function forEachSorted(obj, iterator, context) {
          var keys = sortedKeys(obj);
          for (var i = 0; i < keys.length; i++) {
            iterator.call(context, obj[keys[i]], keys[i]);
          }
          return keys;
        }

        function sortedKeys(obj) {
          var keys = [];
          for (var key in obj) {
            if (obj.hasOwnProperty(key)) {
              keys.push(key);
            }
          }
          return keys.sort();
        }

        if (!params) return url;
        var parts = [];
        forEachSorted(params, function (value, key) {
          if (value == null || value == undefined) return;
          if (angular.isObject(value)) {
            value = angular.toJson(value);
          }
          parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
        });
        return url + ((url.indexOf('?') == -1) ? '?' : '&') + parts.join('&');
      };
      return f;
    }]);

  var tinypassService = angular.module("tinypassServices", []);

  function _setCookie(name, value, expires, path, domain, secure, samesite) {
    // set time, it's in milliseconds
    var today = new Date();
    today.setTime(today.getTime());

    var expires_date = new Date(today.getTime() + (expires));

    document.cookie = name + "=" + value +
      ((expires) ? ";expires=" + expires_date.toGMTString() : "") +
      ((path) ? ";path=" + path : "") +
      ((domain) ? ";domain=" + domain : "") +
      ((secure) ? ";secure" : "") +
      ((samesite) ? ";samesite=" + samesite : "none");
  }

  function _getCookie(name) {

    var matches = document.cookie.match(new RegExp(
      "(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)"
    ))
    return matches ? decodeURIComponent(matches[1]) : undefined
  }

  tinypassService.factory('TinypassService', ['$q', function ($q) {
    var cookieAttrs;
    function _getCookieSameSiteAndSecureAttrString() {
      if (cookieAttrs) {
        return cookieAttrs;
      }
      cookieAttrs = _inIframe() ? ' samesite=none; ' : ' samesite=lax; ';
      if (window.location.protocol === 'https:') {
        cookieAttrs += ' secure;';
      }
      return cookieAttrs;
    }
    function _getTopDomain() {
      var i, h,
        weird_cookie = 'weird_get_top_level_domain=cookie',
        hostname = document.location.hostname.split('.');

      if (hostname.length === 1) {
        return hostname[0];
      }
      if (hostname.length === 4 && !isNaN(hostname[0]) && !isNaN(hostname[3])) {
        return document.location.hostname;
      }

      for (i = hostname.length - 1; i >= 0; i--) {
        h = '.' + hostname.slice(i).join('.');
        var cookieAttrs = _getCookieSameSiteAndSecureAttrString();
        document.cookie = weird_cookie + ';domain=' + h + ';' + cookieAttrs;
        if (document.cookie.indexOf(weird_cookie) > -1) {
          document.cookie = weird_cookie.split('=')[0] + '=;domain=' + h + ';expires=Thu, 01 Jan 1970 00:00:01 GMT;' + cookieAttrs;
          return h;
        }
      }
    }

    function _inIframe() {
      try {
        return window.self !== window.top;
      } catch (e) {
        return true;
      }
    }

    function _setCookie(cname, cvalue, exdays) {
      var d = new Date();
      d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
      var expires = "expires=" + d.toUTCString();
      document.cookie = cname + '=' + cvalue + '; ' + expires + '; domain= ' + _getTopDomain() + '; path=/;' + _getCookieSameSiteAndSecureAttrString();
    }

    function _getCookie(name) {
      var value = "; " + document.cookie;
      var parts = value.split("; " + name + "=");
      if (parts.length > 1) return parts.pop().split(";").shift();
    }

    return {
      setMessage: function (type, msg) {
        tinypass.setMessageDashboard(type, msg);
      },
      hideMessage: function () {
        tinypass.hideMessage();
      },
      cropImage: function (path, image, type, cropX, cropY, cropWidth, cropHeight, onSuccess, onError) {
        tinypass.cropImage(path, image, type, cropX, cropY, cropWidth, cropHeight, onSuccess, onError);
      },
      getISODate: function (from, to) {
        var startDate, endDate;

        startDate = from ? new Date(from.getFullYear(), from.getMonth(), from.getDate()) : new Date();
        endDate = to ? new Date(to.getFullYear(), to.getMonth(), to.getDate(), 23, 59, 59) : new Date();

        return {
          from: moment(startDate).format("YYYY-MM-DDTHH:mm:ssZZ"),
          to: moment(endDate).format("YYYY-MM-DDTHH:mm:ssZZ")
        };
      },
      reduceZeros: function (num) {
        num = Math.round(num);

        var str = ((num / 1000.0) + "");

        if (num != 0) {
          str = (str + (str.indexOf('.') < 0 ? '.' : '') + "000").replace(/(\..{3}).*$/, "$1");
          str = str.substring(0, str.length - Math.min(3, (num + "").length - 2)).replace(/[.]$/, "");

          str = str.split("").reverse().join("").replace(/((\d{3})(?!($|-|.*[.])))/g, '$1,').split("").reverse().join("");
        }
        return str;
      },
      generateRandomString: function (length) {
        length = length || 10;
        var text = "";
        var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
        for (var i = 0; i < length; i++) {
          text += possible.charAt(Math.floor(Math.random() * possible.length));
        }
        return text;
      },
      generateSuccessMessage: function (msg) {
        return function () {
          tinypass.setMessage('success', msg);
        }
      },
      generateErrorMessage: function (msg) {
        return function () {
          tinypass.setMessage('error', msg);
          return $q.reject();
        }
      },
      showRequestError: function (resp) {
        tinypass.setMessage('error', resp.data.errors[0].msg);
        return $q.reject(resp)
      },
      showRequestErrorDashboard: function (resp) {
        tinypass.setMessageDashboard('error', resp.data.errors[0].msg);
        return $q.reject(resp)
      },
      // Converts decimal amount to cents. Eg. $1.99 -> 199
      getInMinorUnits: function (amount) {
        if (!amount) {
          return 0;
        }

        return Math.round(amount * 100);
      },
      getCurrencyByCode: function (currencyCode) {
        if (currencyCode == '') {
          return '$'
        }

        var currency;
        switch (currencyCode) {
          case 'USD':
            currency = '$';
            break;
          case 'EUR':
            currency = '€';
            break;
          case 'AUD':
            currency = '$';
            break;
          case 'GBP':
            currency = '£';
            break;
          case 'JPY':
            currency = '¥';
            break;
          default :
            currency = currencyCode;
            break;
        }
        return currency;
      },
      setCookie: _setCookie,
      getCookie: _getCookie,
      getTopDomain: _getTopDomain,
      getCookieSameSiteAndSecureAttrString: _getCookieSameSiteAndSecureAttrString,
    }
  }]);

  tinypassService.factory('LangUtils', [function () {
    return {
      getPersonalName: function (lang, firstName, lastName) {
        var locale = lang.getLang();
        var isGivenSurnameLang = !['zz_ZZ', 'ja_JP'].some(function (x) {
          return x === locale;
        });

        var personalName;
        if (isGivenSurnameLang) {
          personalName = [firstName, lastName].filter(function (x) {
            return !!x;
          }).join(' ');
        } else {
          personalName = [lastName, firstName].filter(function (x) {
            return !!x;
          }).join(' ');
        }

        return personalName;
      },
    }
  }]);

  tinypassService.factory('modalConfirm', ['$rootScope', '$modal',
    function ($rootScope, $modal) {
      return function (title, msg, res, clickFn) {
        var sc = $rootScope.$new();

        sc.title = title;
        sc.msg = msg;

        var modal = {
          scope: sc,
          templateUrl: '/libs/tinypass/_confirm_popup.shtml'
        };

        var modalInstance = $modal.open(modal);

        sc.onOk = function () {
          //If we pass a function it should return a promise
          //checking that a promise is resolved - close modal
          //if the promise is rejected - do not close modal
          if (clickFn && typeof clickFn === "function") {
            clickFn().then(closeModal);
          } else {
            closeModal(res);
          }
        };

        function closeModal(data) {
          modalInstance.close(data);
        }

        return modalInstance.result;
      }
    }]);

  tinypassService.factory('UpdatePerfectScrollbar', ['UpdateSpecificScrollbar', function (UpdateSpecificScrollbar) {
    return function (event) {
      //We always receive event.target even using in $controller
      if (event && event.target && event.target.nodeName) {
        var $targetElement = $(event.target);
        var targetElementParentHeight;

        var $psContainer;

        //We're assuming search was used and '<input>' inside <div class="dropdown-menu"></div>
        if (event.target.nodeName.toLowerCase() == 'input') {

          var $dropdownContainer = $targetElement.parents('.tp-dropdown__container');
          var $dropdownMenuContainer = $targetElement.parents('.dropdown-menu');

          var $currentContainer = $dropdownMenuContainer.length > 0 ? $dropdownMenuContainer : $dropdownContainer;

          var $containers = $currentContainer.find('.ps-container');
          $containers.each(function () {

            var $container = $(this);
            if ($container.is(':visible')) {
              $container.scrollTop(0)
            }
          });

          return;
        }

        targetElementParentHeight = $targetElement.parent().outerHeight(true);
        $psContainer = $targetElement.parents('.ps-container');

        UpdateSpecificScrollbar($psContainer, targetElementParentHeight);
      }
    };
  }]);

  tinypassService.factory('UpdateSpecificScrollbar', function () {
    return function ($psContainer, targetElementParentHeight) {
      var psContainerChildrenHeight = $psContainer.children().height();
      var psContainerVisibleAreaHeight = $psContainer.height();
      var psContainerScrollTop = $psContainer.scrollTop();

      if (((psContainerScrollTop + psContainerVisibleAreaHeight + targetElementParentHeight) >= psContainerChildrenHeight)
        && (psContainerChildrenHeight >= psContainerVisibleAreaHeight)) {
        $psContainer.scrollTop(psContainerScrollTop - targetElementParentHeight);
      }
    }
  });

  tinypassService.factory('iframeService', ['$rootElement', function ($rootElement) {
    return {
      injectHiddenIframe: injectHiddenIframe,
      injectHiddenIframeWithTimeout: injectHiddenIframeWithTimeout,
      injectIframe: injectIframe,
      injectRealIframe: injectRealIframe,
      injectHungTimeoutIframe: injectHungTimeoutIframe,
      appendContentToIframe: appendContentToIframe,
      getAncestorOrigins: getAncestorOrigins
    };

    function getAncestorOrigins() {
      var ancestorOrigins = document.location.ancestorOrigins;

      if (!ancestorOrigins) {
        var referrer = document.referrer;
        var splitRef = referrer.split('/');
        ancestorOrigins = { 0: splitRef[0] + '//' + splitRef[2], length: 1 };
      }

      return ancestorOrigins;
    }

    function injectHiddenIframe(id, url) {
      var src = url ? ' src="' + url + '" ' : '';

      angular.element("#" + id).remove();

      var iframeInit = angular.element('<iframe id="' + id + '"' + src + ' style="display: none"></iframe>');
      var iframe = iframeInit[0];
      $rootElement.append(iframe);

      return iframe;
    }

    function injectHiddenIframeWithTimeout(id, url, timeoutInterval, timeoutCallback) {
      var iframe = injectHiddenIframe(id, url);

      var killerTimeout = window.setTimeout(function () {
        angular.element("#" + id).remove();

        if (timeoutCallback) {
          timeoutCallback();
        }
      }, timeoutInterval || 15000);

      return {
        iframe: iframe,
        cancel: function () {
          window.clearTimeout(killerTimeout);
        }
      };
    }

    function injectIframe(id, url, $element) {
      var src = url ? ' src="' + url + '" ' : '';

      var iframeInit = angular.element('<iframe ' + src + '></iframe>');
      var cssSettings = {
        backgroundColor: "transparent",
        border: "0",
        width: '100%',
        height: '100%',
        position: 'relative',
        zIndex: 4
      };

      return appendIframe(iframeInit, id, $element, cssSettings);
    }

    function injectRealIframe(id, realIframe, $element, cssSettings) {
      var customCssSettings = {
        backgroundColor: "#ffffff",
        border: "0",
        width: '100%',
        height: '100%',
        position: 'relative',
        zIndex: 1
      };

      Object.assign(customCssSettings, cssSettings)

      return appendIframe(angular.element(realIframe), id, $element, customCssSettings);
    }

    function appendIframe(iframeInit, id, $element, cssSettings) {
      angular.element("#" + id).remove();

      iframeInit.attr({
        allowtransparency: "true",
        id: id
      }).css(cssSettings);
      var iframe = iframeInit[0];
      $element.append(iframe);

      return iframe;
    }

    function injectHungTimeoutIframe(id, replyURL) {
      return window.setTimeout(
        function () {
          var iframe = injectHiddenIframe(id);

          var iframeDocument = iframe.contentWindow ? iframe.contentWindow.document : iframe.contentDocument;
          iframeDocument.open("text/html", "replace");
          iframeDocument.write(
            '<html>' +
            '    <head>' +
            '        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">' +
            '        <script type="text/javascript">' +
            '            function submitform() {' +
            '                document.forms["page_return"].submit();' +
            '            }' +
            '        </script>' +
            '    </head>' +
            '    <body onload="submitform()">' +
            '        <form name="page_return" method="post" action="' + replyURL + '">' +
            '            <input type="hidden" name="resultCode" value="999">' +
            '            <input type="hidden" name="message" value="Request timeout">' +
            '        </form>' +
            '    </body>' +
            '</html>'
          );
          iframeDocument.close();
        },
        15000
      );
    }

    function appendContentToIframe(iframe, content) {
      var iframeDocument = iframe.contentWindow ? iframe.contentWindow.document : iframe.contentDocument;
      iframeDocument.open("text/html", "replace");
      iframeDocument.write(content);
      iframeDocument.close();
    }
  }]);

  var selectFactory = angular.module("selectFactory", ['ajaxServices']);
  selectFactory.factory('selectServiceFactory', ['tpHTTP', '$timeout', function (tpHTTP, $timeout) {
    return function (options) {
      var cache = {};

      var invalidateUpdate = !!options.invalidateUpdate;

      var service = {
        search: search
      };

      angular.extend(service, options);

      return service;

      function loadItems(query) {
        var params = service.buildParams(query);

        return tpHTTP.post(service.endpoint, params)
          .then(service.extractItems)
          .catch(function (rsp) {
          });
      }

      function loadAndCacheItems(query) {
        cache[query] = loadItems(query);
        service.instantUpdate = false;

        if (invalidateUpdate) {
          $timeout(function () {
            delete cache[query];
          }, 1000);
        }

        return cache[query];
      }

      function search(query) {
        var resultPromise = cache[query];

        if (!resultPromise || service.instantUpdate) {
          resultPromise = loadAndCacheItems(query);
        }

        return resultPromise;
      }
    };
  }]);
  selectFactory.factory('selectDirectiveFactory', ['$timeout', 'UpdatePerfectScrollbar', '$perfectScrollBarService',
    function ($timeout, UpdatePerfectScrollbar, $perfectScrollBarService) {
      return function (options) {
        return {
          restrict: 'AE',
          replace: true,
          transclude: true,
          scope: {
            ngModel: '=',
            item: '=',
            clickFn: '=?',
            searchFn: '=?',
            clickOnClose: '=?'
          },
          require: 'ngModel',
          templateUrl: options.templateUrl,
          link: link
        };

        function link($scope, element, attr, ngModelCtrl) {
          var searchWaiting;
          var invokedOnceFor = {};

          $scope.disabled = attr.ngDisabled == 'true';
          if ($scope.disabled) {
            return;
          }
          $scope.closeOnSelect = attr.closeOnSelect !== undefined;
          $scope.multiple = attr.multiple !== undefined;
          $scope.excludeSelected = attr.excludeSelected !== undefined;
          $scope.showCheckbox = $scope.multiple && !$scope.excludeSelected;

          $scope.model = {
            filter: '',
            list: [],
            selected: {}
          };

          $scope.onSearch = onSearch;
          $scope.searchItems = searchItems;
          $scope.updateScrollBar = updateScrollBar;
          $scope.cancelClick = cancelClick;
          $scope.closeDropdown = closeDropdown;
          $scope.setItem = setItem;
          $scope.modelContains = modelContains;
          $scope.setFocusAfterClick = setFocusAfterClick;
          $scope.onOpen = onOpen;
          $scope.selectAll = selectAll;

          onSearch();

          if ($scope.multiple) {
            $scope.$watch('ngModel', function () {
              updateSelectedInList();
            }, true);
          }

          function onOpen(subject) {
            if ($scope.clickFn) {
              $scope.clickFn(searchItems);
            }
            invokeOnce(subject);
          }

          function invokeOnce(subject) {
            if (subject && !invokedOnceFor[subject]) {
              invokedOnceFor[subject] = true;
              $perfectScrollBarService.notify('update-perfect-scrollbar');
            }
          }

          function onSearch($event) {

            if (searchWaiting) {
              $timeout.cancel(searchWaiting);
            }
            var secondsWaiting = (!$scope.model.filter) ? 0 : 300;

            searchWaiting = $timeout(function () {
              searchItems($event);
            }, secondsWaiting);
          }

          function selectAll() {
            if (!$scope.multiple) return;

            var selected = $scope.ngModel || [];
            var list = [];
            if (!selected.length) {
              list = angular.copy($scope.model.list);
            }
            $scope.ngModel = list;
            ngModelCtrl.$setViewValue($scope.ngModel);
          }

          function setItem(item, $event) {
            if (item.disabled !== undefined && item.disabled) {
              return;
            }

            if ($scope.multiple) {
              var list = angular.copy($scope.ngModel || []);
              var itemIndex = findItemIndex(item, list);
              if (itemIndex === null) {
                list.push(item);
              } else {
                list.splice(itemIndex, 1);
              }
              $scope.ngModel = list;
              ngModelCtrl.$setViewValue($scope.ngModel);
            } else {
              $scope.ngModel = item;
              ngModelCtrl.$setViewValue($scope.ngModel);
            }

            if ($scope.closeOnSelect) {
              $scope.closeDropdown();
              if ($scope.clickOnClose) {
                $scope.clickOnClose($scope.ngModel);
              }
            }
            updateScrollBar($event)
          }

          function modelContains(item) {
            var index = findItemIndex(item, $scope.ngModel || []);
            return index !== null;
          }

          function findItemIndex(item, list) {
            var length = list.length;
            for (var i = 0; i < length; i++) {
              if (list[i][options.idProperty] === item[options.idProperty]) return i;
            }
            return null;
          }

          function closeDropdown() {
            $('.dropdown.open', element).removeClass('open');
          }

          function cancelClick(event) {
            event.stopPropagation();
            event.preventDefault();
          }

          function updateScrollBar($event) {
            UpdatePerfectScrollbar($event);
          }

          function searchItems($event) {
            var searchFn = $scope.searchFn || options.searchService.search;
            if (!searchFn) return;

            searchFn($scope.model.filter).then(function (items) {
              items.forEach(function (item) {
                item.id = item[options.idProperty];
              });
              $scope.model.list = items;
              if ($event) {
                $scope.updateScrollBar($event);
              }
            });
          }

          function updateSelectedInList() {
            var selected = {};

            if (!$scope.multiple) {
              return;
            }

            if (!$scope.ngModel || !Array.isArray($scope.ngModel) || $scope.ngModel.length <= 0) {
              $scope.model.selected = null;
              return;
            }

            $scope.ngModel.forEach(function (item) {
              selected[item[options.idProperty]] = true;
            });

            $scope.model.selected = selected;
          }

          function setFocusAfterClick() {
            $('input:first', element).focus();
          }
        }
      };
    }]);

  var loggingModule = angular.module("loggingModule", ['ajaxServices']);
  loggingModule.factory('loggingService', function (tpHTTP) {

    var service = {};
    service.logToServer = function (errorMessage) {
      try {
        var error = new Error;
        tpHTTP.post(TPParam.LOG_ERROR, {
          description: errorMessage,
          url: window.location.href,
          trace: error.stack
        });
      } catch (e) {
      }
    };

    return service;
  });

  angular.module("paymentFormModule", [])
    .factory('loaderService', function () {
      return {
        showLoader: showLoader,
        hideLoader: hideLoader
      }

      function showLoader() {
        var loader = document.getElementById('pp-loader');
        if (loader) {
          loader.style.display = 'block';
        }
      }

      function hideLoader() {
        var loader = document.getElementById('pp-loader');
        if (loader) {
          loader.style.display = 'none';
        }
      }
    })
})();

function html5ModeConfig($locationProvider) {
  if (!(window.history && history.pushState)) {
    $locationProvider.html5Mode(false).hashPrefix('!');
  } else {
    $locationProvider.html5Mode(true);
  }
}

/**
 * The function html5ModeConfig is global and it was set as window's property.
 * But jest uses the jsdom testEnvironment and it wasn't happening,
 * so we set needed global variables/functions manually to use them in tests.
 */
window.html5ModeConfig = html5ModeConfig;

String.prototype.format = function () {
  var content = this;
  for (var i = 0; i < arguments.length; i++) {
    var replacement = '{' + i + '}';
    content = content.replace(replacement, arguments[i]);
  }
  return content;
};

(function(window){
    var _metaTimerID;
    var _metaTimeoutMillis;

    if (typeof(window.tinypass) === 'undefined') window.tinypass = {};

    tinypass.uploadFile2 = function (path, callback) {};

    tinypass.setMessageDashboard = function(type, message) {
        if(type==='error') {
            return tinypass.__setErrorMessageDashboard(message);
        } else {
            return tinypass.__setMessage(type, message);
        }
    };

    tinypass.__buildAlertPopupDashboard = function(type, message) {
        if (message) {
            var alertTop = $(
                '<div class="notification error">' +
                '<span class="message">'+message+'</span>' +
                '<button type="button" class="close close-alert-dashboard">&times;</button>' +
                '</div>');
            $(document.body).append(alertTop);

            alertTop.css('marginLeft',-alertTop.outerWidth()/2);
            alertTop.addClass('in');

            $('.close-alert-dashboard').unbind('click').click(function() {
                $(this).parent().remove();
                tinypass.__buildAlertPopupDashboard();
            });
        }

        var errorNotifi = $('.notification');
        var height = 0;
        var temp = null;
        for (var i = 0; i < errorNotifi.length; i++) {
            temp = $(errorNotifi[i]);
            if (i !== 0) {
                $(errorNotifi[i]).css({ top: 90 + height + 'px' });
            } else {
                $(errorNotifi[i]).css({ top: 90 + 'px' });
            }
            height += $(errorNotifi[i]).outerHeight() + 10;
        }
    };

    tinypass.__setErrorMessageDashboard = function(message) {
        if($('.modal-body').is(":visible")) {
            $('.modal-body  .alert').remove();
            $('.modal-body').prepend(tinypass.__buildAlertPopup('error', message));
        } else if($('#popup').is(":visible")) {
            $('#popup #popup-content .alert').remove();
            $('#popup #popup-content').prepend(tinypass.__buildAlertPopup('error', message));
        } else if ($('.modal .modal-body').length > 0) {
            var modals = $('.modal .modal-body');
            var found = false;
            for (var i = 0; i < modals.length; i++) {
                var modal = modals[i];
                if ($(modal).is(":visible")) {
                    $('.alert', modal).remove();
                    $(modal).prepend($('<div class="row"></div>').append(tinypass.__buildAlertPopup('error', message)));
                    found = true;
                }
            }
            if (!found) {
                if ($('.container .main').length > 0) {
                    tinypass.__buildAlertPopupDashboard('error', message);
                } else {
                    tinypass.__setMessage('error', message);
                }
            }
        } else if ($('.container .main').length > 0) {
            tinypass.__buildAlertPopupDashboard('error', message);
        } else if($('#payscreen #alert').length>0) {
            var alertElement = $('#payscreen #alert');
            alertElement.html(message);
        } else {
            tinypass.__setMessage('error', message);
        }
    };
    tinypass.setMessage = function (type, message) {
        if(type==='error') {
            return tinypass.__setErrorMessage(message);
        } else {
            return tinypass.__setMessage(type, message);
        }
    };
    tinypass.hideMessage = function() {
        $('.alert-top').remove();
        // hide errors too
        $('.tinypass-alert-messase').remove();
        $('.notification.error').remove();
    };

    tinypass.__buildAlertPopup = function(type, message) {
        return $('<div class="alert alert-block alert-' + type + ' tinypass-alert-messase"></div>')
            .append('<button type="button" class="close" data-dismiss="alert">&times;</button>')
            .append($('<span class="message"></span>').text(message));
    };

    tinypass.__setErrorMessage = function (message) {
        if($('.modal-body').is(":visible")) {
            $('.modal-body  .alert').remove();
            $('.modal-body').prepend(tinypass.__buildAlertPopup('error', message));
        } else if($('#popup').is(":visible")) {
            $('#popup #popup-content .alert').remove();
            $('#popup #popup-content').prepend(tinypass.__buildAlertPopup('error', message));
        } else if ($('.modal .modal-body').length > 0) {
            var modals = $('.modal .modal-body');
            var found = false;
            for (var i = 0; i < modals.length; i++) {
                var modal = modals[i];
                if ($(modal).is(":visible")) {
                    $('.alert', modal).remove();
                    $(modal).prepend($('<div class="row"></div>').append(tinypass.__buildAlertPopup('error', message)));
                    found = true;
                }
            }
            if (!found) {
                if ($('.container .main').length > 0) {
                    $('.container .main .alert').remove();
                    $('.container .main').prepend($('<div class="row"></div>').append(tinypass.__buildAlertPopup('error', message)));

                } else {
                    tinypass.__setMessage('error', message);
                }
            }
        } else if ($('.container .main').length > 0) {
            $('.container .main .alert').remove();
            $('.container .main').prepend($('<div class="row"></div>').append(tinypass.__buildAlertPopup('error', message)));
            setTimeout(function() {
                $('.container .main .alert').fadeOut(2000);
            }, 1000);
        } else if($('#payscreen #alert').length>0) {
            var alertElement = $('#payscreen #alert');
            alertElement.html(message);
        } else {
            tinypass.__setMessage('error', message);
        }
    };
    tinypass.__setMessage = function (type, message, permanent) {
        var alertTop = $('<div class="notification">' +
            '<i class="icon-check-white status"></i>' +
            '<span class="message">'+message+'</span>' +
            '</div>');
        $(document.body).append(alertTop);
        if (window.innerWidth > 648) {
            alertTop.css('marginLeft', -alertTop.outerWidth() / 2);
        }
        alertTop.addClass('in');
        setTimeout(function() {
            alertTop.removeClass('in');
        }, 2500);
        setTimeout(function() {
            alertTop.remove();
        }, 5000);
    };

    tinypass.setOpacity = function (elem, fraction) {
        $(elem).css('filter', 'alpha(opacity=' + fraction * 100 + ')')
            .css('-khtml-opacity', fraction)
            .css('-moz-opacity', fraction)
            .css('opacity', fraction);
    }

    tinypass.showWaitPanel = function (config) {
        if (!config) config = {};
//    tinypass.hideMessage();
        tinypass.hideWaitPanel();
        $('body').append($('<div id="waitPanel">' + (config.title ? config.title : 'Loading...') + '</div>').css('display', 'block'));
        if (config.modal) {
            var $e = $('<div id="tinypassModalWindow"/>');
            $e.css('position', 'fixed').css('left', '0').css('top', '0').css('width', '100%').css('height', '100%').css('z-index', '1000');
            if (!config.opacity) {
                $e.css('background-color', 'transparent');
            } else {
                tinypass.setOpacity($e.css('background-color', config.background ? config.background : 'white'), config.opacity);
            }
            $('body').append($e);
        }
    };
    tinypass.hideWaitPanel = function () {
        $("#waitPanel").remove();
        $('#tinypassModalWindow').remove();
    };

    tinypass.__buildPopup = function() {
        return $(
            '<div id="popup" class="modal hide fade" tabindex="-1" role="dialog" aria-hidden="true" style="display: none;">' +
            '<div id="popup-title" class="modal-header"></div>' +
            '<div id="popup-content" class="modal-body"></div>' +
            '<div id="popup-controls" class="modal-footer"></div>' +
            '</div>'
        );
    }

    tinypass.updateMetaRefresh = function(timeoutInMillis){

        if(timeoutInMillis)
            _metaTimeoutMillis = timeoutInMillis;

        if(_metaTimeoutMillis && !isNaN(_metaTimeoutMillis) ) {
            clearTimeout(_metaTimerID);
            _metaTimerID= setTimeout('window.location.reload()', _metaTimeoutMillis);
        }
    };

    tinypass.__initNewlyAddedContent = function(elem) {
        tinypass.__initToolTips(elem);
    };

    $(document).ready(function () {
        var $body = $('body');
        $body.append(tinypass.__buildPopup());

        $body.ajaxStart(function () {
            tinypass.updateMetaRefresh();
            tinypass.showWaitPanel();
        });
        $body.ajaxStop(function () {
            tinypass.hideWaitPanel();
        });

        $('table.row-alternate').each(function () {
            $(this).find('tr:even').find('td').toggleClass('line-item-alt', true);
            $(this).find('tr:odd').find('td').toggleClass('line-item-alt2', true);
        });

        tinypass.__initNewlyAddedContent($body);

        (function ($) {
            var map = new Array();
            $.Watermark = {
                ShowAll: function () {
                    for (var i = 0; i < map.length; i++) {
                        if (map[i].obj.val() === "") {
                            map[i].obj.val(map[i].text);
                            map[i].obj.toggleClass("watermark", true);
                        } else {
                            map[i].obj.toggleClass("watermark", false);
                        }
                    }
                },
                HideAll: function () {
                    for (var i = 0; i < map.length; i++) {
                        if (map[i].obj.val() === map[i].text)
                            map[i].obj.val("");
                    }
                }
            };

            $.fn.Watermark = function (text) {
                return this.each(
                    function () {
                        var input = $(this);
                        map[map.length] = {text: text, obj: input};
                        function clearMessage() {
                            if (input.hasClass("watermark")) {
                                input.toggleClass("watermark", false).val("");
                            }
                        }

                        function insertMessage() {
                            if (input.val().length === 0) {
                                input.toggleClass("watermark", true).val(text);
                            }
                        }

                        input.focus(clearMessage);
                        input.blur(insertMessage);

                        insertMessage();
                    }
                );
            };
        })(jQuery);
    });


    tinypass.__initToolTips = function(elem) {
        $(elem).find('*[data-toggle="tooltip"]').each(function() {
            var me = $(this);
            if(!me.attr('title')) me.attr('title', me.html());
            me.tooltip({html:true});
        });
    };

    tinypass.hasFlash = function() {
        try {
            return ZeroClipboard.detectFlashSupport();
        } catch(e) {}
        return false;
    };

    /*  using ZeroClipboard: https://github.com/jonrohan/ZeroClipboard/blob/master/docs/instructions.md */
    tinypass.copyToClipboard = function(HTMLElement,options) {
        var clip,elem,_config;
        try {
            if (!tinypass.hasFlash()) { /* what do we want to do here? */ return null;}

            elem = $(HTMLElement);
            _config = {}
            _config['moviePath']='/js/ZeroClipboard.swf';
            for (var o in options) {
                _config[o] = options[o];
            }
            clip = new ZeroClipboard(elem,_config);
            clip.glue(elem);

            return clip;
        } catch(e) {}
    };

    tinypass.initImageCropper = function (selector, options, cropWidth, cropHeight) {};

    tinypass.cropImage = function (path, image, type, aid, cropX, cropY, cropWidth, cropHeight, onSuccess, onError) {}

// Strips spaces from a string.
    tinypass.stripSpaces = function(str) {

        if (str === null || typeof str === 'undefined')
            return str;

        return str.replace(/\s+/g, '');
    };

    tinypass.ccLuhnCheck = function(CardNumber) {
        if (!CardNumber.match(/^\d+$/)) return false;
        var no_digit = CardNumber.length;
        var oddoeven = no_digit & 1;
        var sum = 0;

        for (var count = 0; count < no_digit; count++) {
            var digit = parseInt(CardNumber.charAt(count));
            if (!((count & 1) ^ oddoeven)) {
                digit *= 2;
                if (digit > 9)
                    digit -= 9;
            }
            sum += digit;
        }
        return (sum % 10 === 0);
    };

    tinypass.convertCurrencyAndFormat = function(value, rate) {
        try {
            if(!value || value.match(/.*[^0-9.].*/)) return '?.??';
            if(!rate || rate.match(/.*[^0-9.].*/)) return '?.??';
            var v = parseFloat(value) / parseFloat(rate);
            if(v===0 || isNaN(v)) return '?.??';
            v = (Math.ceil(Math.round(v*10000)/100)/100).toFixed(2).replace(/[.]00$/, "");
            return v;
        } catch (e) {
            return '?.??';
        }
    };


    try {
        $(document).ready(function () {
            $(window).on("resize", function(){
                $('#left-nav').find('#left-nav-panel').height($(window).height() - 121);
            });
            $(window).trigger("resize");
        })
    } catch (e) {
    }
})(window);

var pnErrorWatcher = (function () {
  /**
   * ErrorWatcher
   * @constructor
   */
  function ErrorWatcher() {
    /**
     * Happened errors.
     * @type {Array.<ErrorDetails>}
     */
    var privateErrors = [];

    function isInArray(errorsArray, errorObject) {
      var strings = errorsArray.map(JSON.stringify);
      return strings.indexOf(JSON.stringify(errorObject)) !== -1;
    }

    /**
     * @param {Error|Event} error
     */
    function onError(error) {
      var details = {
        userAgent: navigator.userAgent
      };

      if (error instanceof Error) {
        details.stack = error.stack;
        details.message = error.message;
      }

      if (error instanceof Event) {
        var event = error;
        var hasError = (('error' in event) && event.error);
        details.stack = hasError ? event.error.stack : 'n/a';
        details.message = hasError ? event.error.message : 'n/a';
      }

      var widgetType = getParameterByName('widget');
      details.tags = widgetType ? [widgetType]: ['unknown'];

      // do not allow duplicates
      if (isInArray(privateErrors, details)) {
        return;
      }

      privateErrors.push(details);
    }

    function errorEventListener(errorEvent) {
      try {
        onError(errorEvent);
      } catch (e) {
        console.warn('Can not log an error, something went wrong: ', e);
      }
    }

    /**
     * Manually notify error watcher about happened error. Useful in try{} catch {} blocks
     * @param {Error} error
     */
    this.manualLog = function (error) {
      try {
        onError(error);
      } catch (e) {
        console.warn('Can not log an error, something went wrong: ', e, error);
      }
    };

    this.watchUnhandled = function () {
      if (window.addEventListener) {
        window.addEventListener('error', errorEventListener);
      }
    };

    this.reset = function () {
      window.removeEventListener('error', errorEventListener);
    };

    /**
     * Get watched errors
     */
    this.getErrors = function () {
      return privateErrors
        // return immutable data
        .map(function (value) {
          return Object.assign({}, value)
        })
    };
  }

  return new ErrorWatcher();
}());

// Error handler
// Should go first, before an app script
var errorHandler = (function (errorWatcher) {
  function PostMessageEvent() {
    this.sender = null;
    this.event = null;
    this.params = null;
  }

  var POSTMESSAGE_ERRORS_TIMEOUT = 10000;

  function ErrorHandler() {
    var config = {
      /**
       * An array of regexps
       */
      whitelistUrls: [],
    };

    this.setConfig = function (_config) {
      config = Object.assign(config, _config);
    };

    function __getErrors() {
      return errorWatcher.getErrors().filter(function (error) {
        return config.whitelistUrls.some(function (regexp) {
          return error.stack && error.stack.match(regexp);
        });
      });
    }

    function __postMessageToParent(errorsArray) {
      try {
        var target = window.opener || window.parent;

        var postMessage = Object.assign(new PostMessageEvent(), {
          sender: getParameterByName('iframeId'),
          event: 'EVENT_TP_ERROR_HANDLER',
          params: errorsArray,
        });

        if (target) {
          target.postMessage(JSON.stringify(postMessage), '*');
        }

      } catch (e) {
        console.warn('Can not post message errors to parent: ', e);
      }
    }

    function __sendErrors() {
      try {
        var errorsArray = __getErrors();
        if (errorsArray.length === 0) {
          return
        }
        __postMessageToParent(errorsArray);
      } catch (e) {
        console.warn('Errors can not be sent: ', e);
      }
    }

    function __reset() {
      errorWatcher.reset();
    }

    function __start() {
      errorWatcher.watchUnhandled();
      setTimeout(function () {
        __sendErrors();
        __reset();
      }, POSTMESSAGE_ERRORS_TIMEOUT)
    }

    this.start = function () {
      try {
        __start();
      } catch (e) {
        console.warn('Error logger can not be started: ', e);
      }
    };
  }


  try {
    return new ErrorHandler();
  } catch (e) {
    console.warn('Can\'t initiate errorHandler: ', e);
  }
})(pnErrorWatcher);

(function (errorHandler) {
  try {
    var whiteListUrls = [/\w*\.tinypass\.com/, /localhost/];

    var testMode = localStorage.getItem('__tp-tinypassErrorHandlerTestMode');
    var currentHostName = location && location.hostname;
    if (testMode && currentHostName) {
      whiteListUrls.push(new RegExp(currentHostName));
    }

    errorHandler.setConfig({
      whitelistUrls: whiteListUrls,
    });

    errorHandler.start();
  } catch (e) {
    console.warn('Can\'t configure errorHandler: ', e);
  }
})(errorHandler);

angular.module('exceptionHandler', [])
  .factory('$exceptionHandler', ['$log', function ($log) {
    return function exceptionHandler(exception, cause) {
      $log.warn(exception);

      var errorHandler = window.pnErrorWatcher;

      if (errorHandler) {
        errorHandler.manualLog(exception);
      }
    };
  }]);

// whole list of PPs
window.PP_LIST = {
  PAYPAL: 1,
  CREDIT_CARD: 4,
  AMAZON: 8,
  COINBASE: 9,
  MOCK: 5,
  ZERO: 0,
  PAYPAL_BT: 11,
  WORLDPAY_HPP: 12,
  WORLDPAY_PAYPAL: 13,
  WORLDPAY_IDEAL: 14,
  WORLDPAY_ELV: 15,
  SPREEDLY_CC: 16,
  SPREEDLY_STRIPE_CC: 17,
  SPREEDLY_BEANSTREAM: 18,
  EDGIL_PAYWAY: 19,
  WORLDPAY_CC_TOKEN: 20,
  SPREEDLY_PAYU_LATAM: 21,
  PAYPAL_EXPRESS_CHECKOUT: 22,
  SPREEDLY_OPENPAY: 23,
  EIGEN: 24,
  APPLE_PAY_BT: 25,
  OPENPAY_CASH: 26,
  EASYPAY_MULTIBANCO: 28,
  EASYPAY_MBWAY: 29,
  EASYPAY_DIRECT_DEBIT: 30,
  EASYPAY_BOLETO: 31,
  KLARNA: 32,
  OBI: 33,
  OBI_PAYPAL: 34,
  DATATRANS: 35,
  DATATRANS_POSTFINANCE: 36,
  ONET: 37,
  APPLE_PAY_SS: 38,
  CYBER_SOURCE: 39,
  STRIPE: 40,
  STRIPE_APPLEPAY: 41,
  PAYWAY_APPLEPAY: 42,
  WIRECARD: 45,
  CREDIT_GUARD_CC: 66,
  PAY_U_BRAZIL_BOLETO: 62,
  VOLGA: 63
};
angular.extend(window, window.PP_LIST);

var generalModule = angular.module('generalModule', ['ui.router', 'ajaxServices', 'ngSanitize']);

/**
 * Helper methods
 */
function getMonths() {
  return {
    "01": "1 - Jan",
    "02": "2 - Feb",
    "03": "3 - Mar",
    "04": "4 - Apr",
    "05": "5 - May",
    "06": "6 - Jun",
    "07": "7 - Jul",
    "08": "8 - Aug",
    "09": "9 - Sept",
    "10": "10 - Oct",
    "11": "11 - Nov",
    "12": "12 - Dec"
  }
}

generalModule.factory('cookieService', function () {
  var cs = {};

  cs.getCookie = function (name) {
    var _name = name + '=';
    var decodedCookie = decodeURIComponent(document.cookie);
    var ca = decodedCookie.split(';');
    for (var i = 0; i < ca.length; i++) {
      var c = ca[i];
      while (c.charAt(0) === ' ') {
        c = c.substring(1);
      }
      if (c.indexOf(_name) === 0) {
        return c.substring(_name.length, c.length);
      }
    }
    return '';
  };

  cs.setCookie = function (name, value, expires, path) {
    var cookie = name + '=' + value + ';expires=' + expires;
    if (path) {
      cookie += ';path=' + path;
    }
    document.cookie = cookie;
  };

  cs.eraseCookie = function (name, path) {
    this.setCookie(name, '', new Date().toUTCString(), path);
  };

  return cs;
});

function getYears() {
  var d = new Date().getFullYear();
  var years = {};
  for (var i = d; i < d + 13; ++i) {
    var y = i + "";
    years[y.substring(2)] = i;
  }
  return years;
}

function isString(value) {
  return typeof value === 'string';
}

function isNumber(value) {
  return value && value !== '' && !isNaN(value);
}

function lowercase(string) {
  return isString(string) ? string.toLowerCase() : string;
}

function toBoolean(value) {
  if (typeof value === 'function') {
    value = true;
  } else if (value && value.length !== 0) {
    var v = lowercase("" + value);
    value = !(v === 'f' || v === '0' || v === 'false' || v === 'no' || v === 'n' || v === '[]');
  } else {
    value = false;
  }
  return value;
}

/**
 * Gets a random string
 * @param length The length of the string
 * @returns {string} The random string
 */
function _randomString(length) {
  if (!length) {
    length = 5;
  }

  var text = "";
  var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

  for (var i = 0; i < length; i++) {
    text += possible.charAt(Math.floor(Math.random() * possible.length));
  }

  return text;
}

var util = {
  debug: getParameterByName('debug'),
  log: function () {
    if (util.debug) {
      if (typeof (window.console) !== "undefined" && typeof (window.console.log) !== "undefined") {
        var newArgs = arguments;
        if (isBot()) {
          newArgs = buildSeleniumLogMessage(newArgs);
        }
        if (typeof window.console.log === "object") {
          window.console.log.apply.call(console.log, console, newArgs);
        } else {
          console.log.apply(console, newArgs);
        }
      }
    }

    function buildSeleniumLogMessage(args) {
      // selenium log. It can show only first argument
      var seleniumMessage = "";
      try {
        for (var i = 0; i < args.length; i++) {
          try {
            seleniumMessage += JSON.stringify(args[i]) + ", ";

          } catch (e) {
            seleniumMessage += "" + e;
          }
        }
        return args = [seleniumMessage]
      } catch (e) {
        return args
      }
    }

    function isBot() {
      var documentDetectionKeys = [
        "__webdriver_evaluate",
        "__selenium_evaluate",
        "__webdriver_script_function",
        "__webdriver_script_func",
        "__webdriver_script_fn",
        "__fxdriver_evaluate",
        "__driver_unwrapped",
        "__webdriver_unwrapped",
        "__driver_evaluate",
        "__selenium_unwrapped",
        "__fxdriver_unwrapped",
      ];

      var windowDetectionKeys = [
        "_phantom",
        "__nightmare",
        "_selenium",
        "callPhantom",
        "callSelenium",
        "_Selenium_IDE_Recorder",
      ];

      for (var windowDetectionKey in windowDetectionKeys) {
        var windowDetectionKeyValue = windowDetectionKeys[windowDetectionKey];
        if (window[windowDetectionKeyValue]) {
          return true;
        }
      }
      for (var documentDetectionKey in documentDetectionKeys) {
        var documentDetectionKeyValue = documentDetectionKeys[documentDetectionKey];
        if (window['document'][documentDetectionKeyValue]) {
          return true;
        }
      }

      for (var documentKey in window['document']) {
        if (documentKey.match(/\$[a-z]dc_/) && window['document'][documentKey]['cache_']) {
          return true;
        }
      }

      if (window['external'] && window['external'].toString && window['external'].toString() && (window['external'].toString()['indexOf']('Sequentum') !== -1)) return true;

      if (window['document']['documentElement']['getAttribute']('selenium')) return true;
      if (window['document']['documentElement']['getAttribute']('webdriver')) return true;
      if (window['document']['documentElement']['getAttribute']('driver')) return true;

      return false;
    }

  }
};

var urlParams = null;

function getParameterByName(name) {
  if (!urlParams) {
    var match,
      pl = /\+/g,  // Regex for replacing addition symbol with a space
      search = /([^&=]+)=?([^&]*)/g,
      decode = function (s) {
        return decodeURIComponent(s.replace(pl, " "));
      },
      query = window.location.search.substring(1);

    urlParams = {};
    while (match = search.exec(query)) {
      urlParams[decode(match[1])] = decode(match[2]);
    }
  }

  var value = urlParams[name];
  if (!value && window.TPParam && window.TPParam.params) {
    value = window.TPParam.params[name];
  }
  return value;
}

function getQueryParamInStringByName(url, name) {
  var regex = new RegExp('[\\?&]' + name + '=([^&#]*)'),
    results = regex.exec(url);
  return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
}

function getIntParam(name, def) {
  var p = getParameterByName(name);
  if (typeof p !== 'undefined' && isNaN(p))
    return def;
  return new Number(p);
}

BaseComponentController = {
  _isValid: function () {
    return true;
  },

  displayErrors: function ($scope, errors, errorService, eventService) {
    if (!errors) {
      return;
    }

    if (Array.isArray(errors)) {
      for (var i = 0; i < errors.length; i++) {
        let errorMessage = errors[i].msg || errors[i].message;
        if (errorMessage && errorMessage.trim() !== '') {
          handlePaymentError(errorMessage);
        }
      }

      return;
    }

    if (typeof errors === 'object') {
      if (errors.message) {
        handlePaymentError(errors.message);
      } else {
        util.log("Unable to extract error message", errors);
      }

      return;
    }

    if (typeof errors === 'string') {
      handlePaymentError(errors);
    }

    function handlePaymentError(errorMessage) {
      errorService($scope).global(errorMessage);
      eventService.checkoutPaymentErrorEvent(errorMessage);
    }
  }
};

// CHECKOUT EVENTS DEFINITION.
// This evnts if fired using $rootScope.$broadcase(EVENT_NAME, args1, arg2, arg3)
// This should be handled as following:
// $rootScope.$on(EVENT_NAME, function(event, arg1, arg2, arg3) {
//                      // your code here
//               });
/** Event throws when the payment methods list are changed by the checkout process. It used to change on term selection.
 * Event arguments:
 * <ul>
 * <li><b>paymentMethods</b> - list of the new payment methods that were selected. <b>Mandatory.</b></li>
 * </ul>
 */
var EVENT_CHECKOUT_PAYMENT_METHODS_CHANGED = 'EVENT_CHECKOUT_PAYMENT_METHODS_CHANGED';
/** Event fires when term is selected.
 * Event arguments:
 * <ul>
 * <li><b>term</b> - term object that were used. <b>Mandatory.</b></li>
 * </ul>
 */
var EVENT_CHECKOUT_TERM_SELECTED = 'EVENT_CHECKOUT_TERM_SELECTED';
var EVENT_APPLY_REDEEM_CODE = 'EVENT_APPLY_REDEEM_CODE';
var EVENT_CHECKOUT_CONSENT_CHANGED = 'EVENT_CHECKOUT_CONSENT_CHANGED';
var EVENT_CHECKOUT_RESET_SELECTED_PAYMENT_METHOD = 'EVENT_CHECKOUT_RESET_SELECTED_PAYMENT_METHOD';

/**
 *   Event fires when term price changed
 *   args:  {termId: id , price: price}
 */
var EVENT_CHECKOUT_PRICE_CHANGED = "EVENT_CHECKOUT_PRICE_CHANGED";

var EVENT_CHECKOUT_REDEEM_DONE = "EVENT_CHECKOUT_REDEEM_DONE";

var EVENT_APPLE_PAY_CAN_MAKE_PAYMENTS = "EVENT_APPLE_PAY_CAN_MAKE_PAYMENTS";

/**
 * Events fires when the checkout.access onject is changed
 * Event arguments: none
 * @type {string}
 */
var EVENT_BILLING_COUNTRY_SELECTED = "EVENT_BILLING_COUNTRY_SELECTED";
var EVENT_COUNTRY_OF_RESIDENCE_SELECTED = "EVENT_COUNTRY_OF_RESIDENCE_SELECTED";
var EVENT_SAME_RESIDENCE_CHECKBOX_CHANGED = "EVENT_SAME_RESIDENCE_CHECKBOX_CHANGED";
var EVENT_BILLING_FROM_TAX_REQUEST = "EVENT_BILLING_FROM_TAX_REQUEST";
var EVENT_CC_ZIP_CODE_CHANGED = "EVENT_CC_ZIP_CODE_CHANGED";
var EVENT_USER_ACCESS_CHANGED = "EVENT_USER_ACCESS_CHANGED";
var EVENT_ERRORS_CHANGED = "EVENT_ERRORS_CHANGED";
var EVENT_TERMINAL_ERROR = "EVENT_TERMINAL_ERROR";
//var EVENT_VIEW_ACTIVATED = "EVENT_VIEW_ACTIVATED";
var EVENT_CONFIG_RELOAD = "EVENT_CONFIG_RELOAD";
var EVENT_STATS_TRACKED = "EVENT_STATS_TRACKED";
var EVENT_TRACKING_ID_CHANGED = "EVENT_TRACKING_ID_CHANGED";
var EVENT_PAY_WITH_NEW_CHANGED = "EVENT_PAY_WITH_NEW_CHANGED";

/* should be fired if custom price should be applied along with taxes information */
var EVENT_PAY_WHAT_YOU_WANT_SHOULD_BE_APPLIED = "EVENT_PAY_WHAT_YOU_WANT_SHOULD_BE_APPLIED";
var EVENT_EXPERIAN_ADDRESS_CHANGED = "EVENT_EXPERIAN_ADDRESS_CHANGED";
var EVENT_TAX_RESET_ERRORS = "EVENT_TAX_RESET_ERRORS";
var EVENT_HIGHLIGHT_ERROR_FIELDS = "EVENT_HIGHLIGHT_ERROR_FIELDS";
var ENABLE_SAVE_BUTTON = "ENABLE_SAVE_BUTTON";
var DISABLE_SAVE_BUTTON = "DISABLE_SAVE_BUTTON";

var EVENT_SETUP_CUSTOM_VARIABLES = 'setupCustomVariables';
var EVENT_SETUP_AFFILIATE_STATE = 'setupAffiliateState';
var EVENT_SETUP_EVT_FIELDS = 'setupEVTFields';
var EVENT_ADD_CARD = 'EVENT_ADD_CARD';
var EVENT_INIT_BANCARD = 'EVENT_INIT_BANCARD';
var EVENT_GOTO_PREVIOUS_STATE = 'EVENT_GOTO_PREVIOUS_STATE';

/* template related events */
var EVENT_TEMPLATE_RELOADED = "EVENT_TEMPLATE_RELOADED";
var EVENT_TEMPLATE_LOGIN_CHECK_FINISHED = "EVENT_TEMPLATE_LOGIN_CHECK_FINISHED";
var EVENT_TEMPLATE_LOGIN_SUCCESS = "EVENT_TEMPLATE_LOGIN_SUCCESS";

/**
 * Event service that handles firing and subscription handling for the events
 */
generalModule.factory('ngEventService', ['$rootScope', function ($rootScope) {
  var e = {};

  e.fire = function (eventName, arg1, arg2, arg3, arg4, arg5) {
    if (!eventName || eventName === '') throw new Error('eventName is required');
    util.log("[NG EVENT SERVICE] Firing event:", arguments);
    $rootScope.$broadcast(eventName, arg1, arg2, arg3, arg4, arg5);
  };

  e.subscribe = function (eventName, callback) {
    if (!eventName || eventName === '') throw new Error('eventName is required');
    if (!callback) throw new Error('callback is required');
    return $rootScope.$on(eventName, callback.bind(arguments));
  };

  return e;
}]);

generalModule.factory('stateService', function () {
  var params = TPParam && TPParam.params ? TPParam.params : {};
  var state = {
    // whether offer is active or not
    // (it could be not active when iframe is preloaded but not shown yet)
    active: !params.preload
  };
  return {
    get: function (key) {
      return state[key];
    },
    set: function (key, value) {
      state[key] = value;
    }
  }
});

generalModule.factory('eventLogger', ['$http', '$window', function ($http, $window) {
  var endpoints = {
    log: 'https://api.tinypass.com/api/v3/anon/error/log'
    //log: 'http://localhost:8080/api/v3/anon/error/log'
  };

  function buildMessage(eventName, payload, meta) {
    var payloadAsString;
    var metaAsString;
    var message = [];

    try {
      payloadAsString = (typeof payload === 'string') ? {message: payload} : JSON.stringify(payload);
    } catch (e) {
      payloadAsString = 'parse error';
    }

    try {
      metaAsString = JSON.stringify(meta);
    } catch (e) {
      metaAsString = 'meta error';
    }

    message.push('=====checkout event=====');
    message.push('eventName: ' + eventName);

    if (payload) {
      message.push('payload: ' + payloadAsString);
    }

    message.push('meta: ' + metaAsString);
    message.push('=====event body end=====');

    return message.join('\n');
  }

  function getMeta() {
    var meta = {
      aid: null,
      offerId: null,
      gaTrackingId: null
    };

    var aidMatch = location.href.match(/aid=([\w\d]+)&/);
    var offerIdMatch = location.href.match(/offerId=([\w\d]+)&/);

    if (aidMatch) {
      meta.aid = aidMatch[1];
    }

    if (offerIdMatch) {
      meta.offerId = offerIdMatch[1];
    }

    if ($window.ga) {
      $window.ga(function (tracker) {
        meta.gaTrackingId = tracker.get('trackingId');
      });
    }

    return meta;
  }

  function logEvent(eventName, payload) {
    try {
      var meta = getMeta();
      var message = buildMessage(eventName, payload, meta);

      if (!meta) {
        return;
      }

      if (isLogEventDisabled()) {
        return;
      }

      $http({
        method: 'POST',
        url: endpoints.log,
        data: 'log_message=' + generateLogMessage(message),
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
          'Piano-request-without-spinner': 1
        }
      });

      function isLogEventDisabled() {
        return ['lGr3ciYmC7', '6qv8OniKQO', 'bMMF5VBfpu'].indexOf(meta.aid) === -1 && eventName !== 'edgilHostedPageSubmitForm';
      }

    } catch (e) {
      console.warn("can't log event due to:", e);
    }
  }

  function logGaEvent(event) {
    logEvent('gaEvent', event);
  }

  function logGaFailedEvent(event) {
    logEvent('gaEvent [FAILED]', event);
  }

  function generateLogMessage(message) {
    var widgetType = getParameterByName('widget');
    var log = [
      {
        message: message,
        tags: widgetType ? [widgetType] : []
      }
    ];

    return JSON.stringify(log);
  }

  return {
    logGaEvent: logGaEvent,
    logGaFailedEvent: logGaFailedEvent,
    logEvent: logEvent
  };
}]);

generalModule.factory('gaService', ['$window', 'stateService', 'integrationEventsService', 'eventLogger', 'eventService',
  function ($window, stateService, integrationEventsService, eventLogger, eventService) {
    var noninteractionEvents = [
      'showOffer',
      'termSelected',
      'promoApplied',
      'promoInvalid',
      'loginSuccess',
      'loginFailed',
      'registrationFailed',
      'registrationSuccess',
      'checkoutSuccess'
    ];

    var me = {};

    me.sendGoogleAnalyticsEvent = function (event) {
      if (event) {
        event.eventCategory = event.eventAction;

        var delimiter = " ";
        event.eventAction = "";

        angular.forEach(event.params, function (value, key) {
          if (value) {
            event.eventAction += delimiter + key + "_" + value;
            delimiter = "____";
            event.eventLabel += " " + key + ":" + value;
          }
        });

        var options = {};

        if (noninteractionEvents.indexOf(event.eventCategory) !== -1) {
          options.nonInteraction = true;
        }

        var eventParams = {
          eventCategory: event.eventCategory,
          eventAction: event.eventAction,
          eventLabel: event.eventLabel
        };

        eventService.emitGAEvent(Object.assign(eventParams, options));
        eventLogger.logGaEvent(event);
      }
    };

    return me;
  }]);

generalModule.factory('statsService', ['ngEventService', 'stateService', '$q', '$http',
  function (ngEventService, stateService, $q, $http) {
    var stats = {};
    var statsParams = null;
    var initStatParams = null;
    var statsWaiters = [];
    var statsPayloadWaiters = [];
    var paramsWaiters = [];
    var isTracked = false;
    var checkoutView = null;

    var campaignStatsKey = 'tpcc_';
    var termsStatsKey = 'terms';
    var statsKeys = [
      'aid',
      'preview',
      'userProvider',
      'userToken',
      'url',
      'tags',
      'tbc',
      'contentCreated',
      'contentSection',
      'contentAuthor',
      'experienceId',
      'experienceExecutionId',
      'experienceActionId',
      'pageViewId',
      'visitId',
      'trackingId',
      'previewTemplateVersion',
      'offerId',
      'checkoutFlowId',
      'offerTemplateId',
      'templateId',
      'templateVariantId',
      'offerTemplateVariantId',
      'templatePubId',
      'templateVersionPubId',
      'templateVersion',
      'templateCategory',
      'isPasswordlessCheckoutEnabled',
      'isSingleStepEnabled',
      'isDoubleOptInEnabled',
      'affiliateState'
    ];

    function handleTrackStatEvent(event, params) {
      stateService.set('active', true);

      if (!TPParam.TRACK_SHOW) {
        return;
      }

      initStatParams = createInitStatParams(params, TPParam.config);
      setStats(params, stats, initStatParams);
      notifyWaiters();

      $http(
        {
          method: 'POST',
          url: getTrackShowUrl(),
          data: {
            initStatParams: initStatParams
          },
          headers: {
            'Piano-request-without-spinner': 1
          }
        }
      ).then(function (resp) {
        isTracked = true;
        setStats(params, resp.data.models.stats, initStatParams);
      });

      function getTrackShowUrl() {
        let url = TPParam.TRACK_SHOW;
        let lang = getParameterByName('lang');
        if (lang) {
          url = url + '?lang=' + lang;
        }
        return url;
      }

      function createInitStatParams(params, checkoutConfig) {
        var initStatParams = {};

        extractCommonParams(params, checkoutConfig, initStatParams);
        extractTermIds(params, checkoutConfig, initStatParams);

        extractCampaignParams(checkoutConfig, initStatParams);
        extractCampaignParams(params, initStatParams);

        extractCheckoutViewParam(initStatParams);

        return initStatParams;
      }

      function extractCheckoutViewParam(params) {
        params.checkoutView = checkoutView;
      }

      function extractCommonParams(params, checkoutConfig, stats) {
        statsKeys.forEach(function (value) {
          if (params && params[value]) {
            stats[value] = params[value];
          } else if (checkoutConfig && checkoutConfig[value]) {
            stats[value] = checkoutConfig[value];
          }
        });
      }

      function extractTermIds(params, checkoutConfig, stats) {
        var terms;
        if (params && params[termsStatsKey]) {
          terms = params[termsStatsKey];
        } else if (checkoutConfig && checkoutConfig[termsStatsKey]) {
          terms = checkoutConfig[termsStatsKey];
        }

        if (!Array.isArray(terms)) {
          return;
        }

        var termIds = [];
        terms.forEach(function (term) {
          if (term) {
            termIds.push(term.termId);
          }
        });

        stats.termIds = termIds;
      }

      function extractCampaignParams(params, stats) {
        if (params) {
          $.each(params, function (index, value) {
            if (index.indexOf(campaignStatsKey) === 0) {
              stats[index] = value;
            }
          });
        }
      }

      function setStats(params, stat, initParams) {
        stats = stat;
        statsParams = params;
        initStatParams = initParams;
      }

      function notifyWaiters() {
        notifyStatsWaiters();
        notifyStatsPayloadWaiters();
        notifyParamsWaiters();

        ngEventService.fire(EVENT_STATS_TRACKED, {params: statsParams, stat: stats});
      }

      function isPreview(params) {
        if (params && params.preview) {
          var preview = params.preview;
          return typeof preview === 'string' ? preview === 'true' : preview;
        }
        return false;
      }

      function notifyStatsWaiters() {
        statsWaiters.forEach(function (promise) {
          promise.resolve(createStatsObject());
        });
        statsWaiters = [];
      }

      function notifyStatsPayloadWaiters() {
        statsPayloadWaiters.forEach(function (promise) {
          promise.resolve(createStatsPayloadObject());
        });
        statsPayloadWaiters = [];
      }

      function notifyParamsWaiters() {
        paramsWaiters.forEach(function (promise) {
          promise.resolve(statsParams);
        });
        paramsWaiters = [];
      }
    }

    function getStats() {
      if (initStatParams) {
        return $q.when(createStatsObject());
      }

      var promise = $q.defer();
      statsWaiters.push(promise);
      return promise.promise;
    }

    function getStatsPayload() {
      if (initStatParams) {
        return $q.when(createStatsPayloadObject());
      }

      var promise = $q.defer();
      statsPayloadWaiters.push(promise);
      return promise.promise;
    }

    function createStatsPayloadObject() {
      return isTracked ?
        {checkoutStats: stats} :
        {initStatParams: initStatParams};
    }

    function createStatsObject() {
      return {
        checkoutStats: stats,
        initStatParams: initStatParams
      };
    }

    function getParams() {
      var promise = $q.defer();
      if (statsParams) {
        promise.resolve(statsParams);
      } else {
        paramsWaiters.push(promise);
      }
      return promise.promise;
    }

    function mergeStats(newStats) {
      getStats().then(function () {
        angular.extend(stats, newStats);
      });
    }

    function setCheckoutView(view) {
      checkoutView = view;
    }

    function trackView(view) {
      setCheckoutView(view);

      if (view === "offer") {
        // the offer tracked by track show action
        return;
      }

      if (!TPParam.TRACK_VIEW) {
        return;
      }

      getStatsPayload().then(function (statsPayload) {
        $http({
          method: 'POST',
          url: TPParam.TRACK_VIEW,
          params: {
            view: view
          },
          data: statsPayload,
          headers: {
            'Piano-request-without-spinner': 1
          }
        }).then(function (resp) {
          if (resp && resp.models && resp.models.checkoutStats) {
            mergeStats(resp.models.checkoutStats);
          }
        })
      })
    }

    function getPageView() {
      let currentStats = createStatsObject();
      let tbc = null;
      if (currentStats && currentStats.initStatParams) {
        tbc = currentStats.initStatParams.tbc;
      }

      let pageViewId = null;
      if (currentStats && currentStats.initStatParams) {
        pageViewId = currentStats.initStatParams.pageViewId;
      }

      return {
        tbc: tbc,
        pageViewId: pageViewId,
      };
    }

    return {
      getStatsPayload: getStatsPayload,
      getStats: getStats,
      mergeStats: mergeStats,
      getParams: getParams,
      trackView: trackView,
      setCheckoutView: setCheckoutView,
      getPageView: getPageView,
      handleTrackStatEvent: handleTrackStatEvent
    };
  }]);

generalModule.factory('browserIdService', ['ngEventService', '$q',
  function (ngEventService, $q) {

    var promise = $q.defer();

    ngEventService.subscribe('passBrowserId', function (event, params) {
      promise.resolve(params.browserId);
    });

    function getBrowserId() {
      return promise.promise;
    }

    return {
      getBrowserId: getBrowserId
    };
  }]);

generalModule.factory('configService', ['ngEventService', function (ngEventService) {
  var config = {};
  return {
    get: function () {
      return config;
    },
    setProperty: function (name, value) {
      config[name] = value;

    },
    changed: function () {
      ngEventService.fire(EVENT_CONFIG_RELOAD, config);
    }
  };
}]);

generalModule.factory('apiService', ['$http', function ($http) {
  var endpointUrl = '/api/v3';
  var config = {
    method: 'post',
    headers: {
      'Piano-request-without-spinner': 1
    }
  };
  return {
    call: function (endpoint, params, method) {
      var newConfig = angular.copy(config);
      newConfig.url = endpointUrl + endpoint;

      if (method) {
        newConfig.method = method;
      }
      newConfig.params = params;
      return $http(newConfig);
    }
  };
}]);

generalModule.factory("modalService", ['$rootScope', '$q', '$http', '$templateCache', '$injector', '$timeout', '$document', '$compile', '$controller',
  function ($rootScope, $q, $http, $templateCache, $injector, $timeout, $document, $compile, $controller) {
    var modalService = {};
    var backdrop = {
      created: false,
      visible: false,
      domEl: null
    };
    var modalWindow = {
      created: false,
      scope: null,
      domEl: null
    };
    var tooTall = false;

    var getTemplatePromise = function (options) {
      return options.template ? $q.when(options.template) :
        $http.get(options.templateUrl, {cache: $templateCache}).then(function (result) {
          return result.data;
        });
    };

    var getResolvePromises = function (resolves) {
      var promisesArr = [];
      angular.forEach(resolves, function (value, key) {
        if (angular.isFunction(value) || angular.isArray(value)) {
          promisesArr.push($q.when($injector.invoke(value)));
        }
      });
      return promisesArr;
    };

    var open = function (modalInstance, modal, modalOptions) {
      var body = $document.find('body').eq(0);

      if (!backdrop.created) {
        var backdropScope = $rootScope.$new(true);
        var backdropDomEl = $compile('<div class="modal-overlay"></div> ')(backdropScope);
        body.append(backdropDomEl);
        backdrop.created = true;
        backdrop.domEl = backdropDomEl;
      }

      if (!backdrop.visible) {
        var $backdrop = $(backdrop.domEl);
        var speed = modalOptions.speed || 600;
        $backdrop.addClass('active');
        $backdrop.animate({opacity: 0.7}, speed);
        backdrop.visible = true;
      }

      var angularDomEl = angular.element('<div class="modal ' + (modalOptions['class'] || '') + '"></div>');
      angularDomEl.addClass('active');
      angularDomEl.animate({opacity: 1.0}, speed);
      angularDomEl.html(modal.content);

      var modalDomEl = $compile(angularDomEl)(modal.scope);
      modalWindow.created = true;
      modalWindow.scope = modal.scope;
      modalWindow.domEl = angularDomEl;
      body.append(modalDomEl);
    };

    var closeDialog = function (modalInstance, modalOptions, callbackOnClose) {
      var speed = modalOptions.speed || 600;
      var ease = "swing";
      if (backdrop.created && backdrop.visible) {
        $(backdrop.domEl).animate({opacity: 0}, speed, ease);
        $(backdrop.domEl).removeClass('active');
        backdrop.visible = false;
      }
      if (modalWindow.created) {
        $(modalWindow.domEl).animate({opacity: 0, marginTop: 0}, speed, ease, function () {
          $(modalWindow.domEl).removeClass('active');
          $(modalWindow.domEl).removeClass('error');
          modalWindow.domEl.remove();
          modalWindow.scope.$destroy();
          modalWindow.scope = null;
          modalWindow.domEl = null;
          modalWindow.created = false;

          if (callbackOnClose) {
            callbackOnClose();
          }
        });
      }
    };

    modalService.openDialog = function (modalOptions) {
      var modalInstance = {
        close: function (callbackOnClose) {
          closeDialog(modalInstance, modalOptions, callbackOnClose);
        }
      };

      util.log("MODAL OPTIONS", modalOptions);

      if (!modalOptions.template && !modalOptions.templateUrl) {
        throw new Error('One of template or templateUrl options is required.');
      }

      var templateAndResolvePromise =
        $q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve)));

      templateAndResolvePromise.then(function (tplAndVars) {
        var modalScope = (modalOptions.scope || $rootScope).$new();
        modalScope.close = modalInstance.close;

        var ctrlInstance, ctrlLocals = {};
        var resolveIter = 1;

        //controllers
        if (modalOptions.controller) {
          ctrlLocals.$scope = modalScope;
          ctrlLocals.$modalInstance = modalInstance;
          angular.forEach(modalOptions.resolve, function (value, key) {
            ctrlLocals[key] = tplAndVars[resolveIter++];
          });

          ctrlInstance = $controller(modalOptions.controller, ctrlLocals);
        }


        open(modalInstance, {
          scope: modalScope,
          content: tplAndVars[0]
        }, modalOptions);

      });
    };
    modalService.getModalEl = function () {
      if (modalWindow) {
        return modalWindow.domEl;
      }
      return null;
    };

    return modalService;
  }]);

generalModule.factory('shakeService', function () {
  var me = {};
  me.shake = function (obj) {
    var l = (Math.random() < 0.5 ? -2 : 2);
    for (var i = 0; i < 10; i++) {
      $(obj).animate({'margin-left': (l = -l) + 'px'}, 10 + (i * 2));
    }
    $(obj).animate({'margin-left': 0}, 5);
  };
  return me;
});

generalModule.factory('EventManager', function () {
  return function () {
    var events = {};

    return {
      emit: emit,
      subscribe: subscribe,
      getEventSubscriptionFunc: getEventSubscriptionFunc,
      unsubscribe: unsubscribe,
    };

    /**
     * @param {string} name
     * @param {*} [data]
     */
    function emit(name, data) {
      validateEventName(name);

      var callbacks = events[name];

      if (!callbacks) {
        console.info('Event `', name, '` with no subscribers was emitted.');
        return;
      }

      callbacks.forEach(function (callback) {
        callback.call(this, data);
      });
    }

    /**
     * @param {string} name
     * @param {function} callback
     */
    function subscribe(name, callback) {
      validateEventName(name);
      validateCallback(callback);

      var callbacks = events[name];

      if (!callbacks) {
        callbacks = events[name] = [];
      }

      callbacks.push(callback);

      return unsubscribe.bind(null, name, callback);
    }

    /**
     * Returns a function which lets you subscribe to the '{@param name}' event.
     * @param {string} name
     */
    function getEventSubscriptionFunc(name) {
      return subscribe.bind(null, name);
    }

    /**
     * Generally you should use the unsubscribe function
     * returned by {@link subscribe}. This method is public only just in case.
     * @param {string} name
     * @param {function} callback
     */
    function unsubscribe(name, callback) {
      validateEventName(name);

      var callbacks = events[name];

      if (!callbacks) {
        console.warn('Attempt to unsubscribe from the unknown event `{0}`.'.format(name));
        return;
      }

      var callbackIndex = callbacks.indexOf(callback);

      if (callbackIndex === -1) {
        console.warn('Given callback was not found for the event `{0}`.'.format(name));
        return;
      }

      callbacks.splice(callbackIndex, 1);
    }

    function validateEventName(name) {
      if (!name) {
        throw new TypeError('Expected event name to be a non-empty string, but got ' + name);
      }
    }

    function validateCallback(callback) {
      if (typeof callback !== 'function') {
        throw new TypeError('Expected callback to be a function, but got ' + callback);
      }
    }
  }
});

generalModule.directive('customScript', ['$parse', function ($parse) {
  return {
    restrict: 'EA',
    scope: {},
    link: function (scope, element) {
      function replaceVariables(replacement) {
        var variableKey = replacement.substring(2, replacement.length - 1);
        var scopeValue = $parse(variableKey)(scope.$parent);

        return typeof scopeValue === 'string' ? '\"' + scopeValue + '\"' : scopeValue;
      }

      function interpolateParentScopeVars(scriptText) {
        if (scriptText) {
          return scriptText.replace(/#{(.*?)}/g, replaceVariables);
        }

        return scriptText;
      }

      function revertSpecialSymbols(scriptText) {
        return scriptText
          .replace(/&lt;/g, '<')
          .replace(/&gt;/g, '>')
          .replace(/&amp;/g, '&')
          .replace(/&quot;/g, '"');
      }

      function wrapIntoTryCatch(scriptText) {
        try {
          scriptText = revertSpecialSymbols(interpolateParentScopeVars(scriptText));
        } catch (e) {
          console.error(e);
        }

        return 'try {\n' + scriptText + '\n} catch (e) { console.error(e); }';
      }

      function appendCustomScript() {
        var script = document.createElement('script');
        var scriptText = wrapIntoTryCatch(element.html());

        script.type = 'text/javascript';
        script.innerHTML = scriptText;

        element.empty();

        document.body.appendChild(script);
      }

      element.ready(appendCustomScript);
    }
  };
}]);

generalModule.directive('boilerplateCloseButton', ['configService', function (configService) {
  return {
    restrict: 'E',
    transclude: true,
    replace: true,
    template: "<div " +
      "ng-if='isShowBoilerplateCloseButton()' " +
      "class='boilerplate-close-button' " +
      "ng-click='close()'></div>",
    link: function (scope, element, attr) {
      scope.isShowBoilerplateCloseButton = function () {
        var closeButtonType = configService.get().closeButtonType;
        var showCloseButton = getParameterByName('showCloseButton') !== 'false';
        var isBoilerplateBtnType = closeButtonType === 'boilerplate';

        return showCloseButton && isBoilerplateBtnType;
      }
    }
  };
}]);

/**
 * Added this because IE doesn't support `position: sticky;`.
 * This is a JS version of a CSS rule
 *     [pn-sticky] {
 *       position: sticky;
 *       bottom: 0;
 *     }
 */
generalModule.directive('pnSticky', [
  '$timeout',
  '$interval',
  'utilsService',
  function (
    $timeout,
    $interval,
    utilsService
  ) {
    return {
      restrict: 'A',
      link: function ($scope, $element) {
        var $parent = $element.parent();
        var parent = $parent[0];

        $parent.scroll(utilsService.throttle(parentScrollHandler, 200));

        // Parent may have scroll from the start, but if it's contents
        // are loaded asynchronously, we don't know when the scroll appears.
        // So we use an interval which is cancelled on the first scroll.
        var initHandlerInterval = $interval(initialChecker, 100, 100);
        var isInitialCheckerCancelled = false;
        var isFixed = false;

        $scope.$on('$destroy', cancelInitialChecker);

        function parentScrollHandler() {
          cancelInitialChecker();

          if (parent.scrollHeight > parent.clientHeight) {
            if (!isFixed) {
              $element.css({
                position: 'fixed',
                bottom: '0',
              });
              isFixed = true;
            }
          } else {
            if (isFixed) {
              $element.css({
                position: 'static',
                bottom: '',
              });
              isFixed = false;
            }
          }
        }

        function initialChecker() {
          if (parent.scrollHeight > parent.clientHeight) {
            $element.css({
              position: 'fixed',
              bottom: '0',
            });
            cancelInitialChecker();
          }
        }

        function cancelInitialChecker() {
          if (!isInitialCheckerCancelled) {
            $interval.cancel(initHandlerInterval);
            isInitialCheckerCancelled = true;
          }
        }
      },
    };
  }
]);

generalModule.factory('windowStateService', [function () {
  function inIframe() {
    try {
      return window.self !== window.top;
    } catch (e) {
      return true;
    }
  }

  function inIframeOrChildWindow() {
    return inIframe() || window.opener;
  }

  return {
    checkIframeOrChildWindow: function () {
//            if (!inIframeOrChildWindow()) {
//                errorService().terminal('Window state is invalid ');
//                return false;
//            }
      return true;
    }
  }
}]);

generalModule.factory('exposeTemplateParams', function () {
  return function (params) {
    var genericProperties = [
      'aid',
      'debug',
      'displayMode',
      'iframeId',
      'offerId',
      'tags',
      'templateId',
      'templateVariantId',
      'url',
      'width'
    ];

    // do not forget to update composer property list in offer.js
    var composerProperties = [
      "trackingId",
      "experienceId",
      "experienceExecutionId",
      "experienceActionId"
    ];

    var paramsToScope = genericProperties.concat(composerProperties);

    var resultParams = {};
    angular.forEach(paramsToScope, function (paramName) {
      if (params[paramName]) {
        resultParams[paramName] = params[paramName];
      }
    });

    return resultParams;
  }
});

generalModule.factory('exposeCustomVariables', function () {
  return function (customVariablesJson) {
    var customVariables = {};
    try {
      customVariables = JSON.parse(customVariablesJson);
    } catch (e) {
    }
    return customVariables;
  };
});

generalModule.factory('exposeCustomCookies', function () {
  return function (customCookiesJson) {
    var customCookies = {};
    try {
      customCookies = JSON.parse(customCookiesJson);
    } catch (e) {
    }
    return customCookies;
  };
});

generalModule.factory('exposeActiveMeters', function () {
  return function (activeMetersJson) {
    var activeMeters = [];
    try {
      activeMeters = JSON.parse(activeMetersJson);
    } catch (e) {
    }
    return activeMeters;
  };
});

generalModule.service('integrationEventsService', function () {
  this.dispatchLocal = function (eventName, data) {
    IntegrationEvents.dispatchLocal(eventName, data);
  };

  // subscribe on events from integration scripts
  this.listenExternal = function (eventName, callback) {
    IntegrationEvents.listenExternal(eventName, callback);
  };

  this.listenLocal = function (eventName, callback) {
    IntegrationEvents.listenLocal(eventName, callback);
  };
});

generalModule.factory('topLocation', ['$window', 'eventService', '$q',
  function ($window, eventService, $q) {
    var topLocation = function () {
      var deferred = $q.defer();

      eventService.postMessage('getLocation', {
        resultCallback: function (params) {
          var parser = $window.document.createElement('a');
          parser.href = params.result;
          deferred.resolve(parser);
        }
      });

      return deferred.promise;
    };
    topLocation.hash = function (value) {
      eventService.postMessage('setLocationHash', {
        hash: value
      });
    };

    return topLocation;
  }]);

generalModule.factory('addQueryParameterToUrl', function () {
  return function (url, queryParameterName, queryParameterValue) {
    var urlFragment;
    var lastUrlIndexBeforeHash;
    if (url.indexOf('#') > 0) {
      lastUrlIndexBeforeHash = url.indexOf('#');
      urlFragment = url.substring(url.indexOf('#'), url.length);
    } else {
      urlFragment = '';
      lastUrlIndexBeforeHash = url.length;
    }

    var urlWithoutFragment = url.substring(0, lastUrlIndexBeforeHash);
    var urlParts = urlWithoutFragment.split('?');

    var newQueryString = '?';
    if (urlParts.length > 1) {
      var parameters = urlParts[1];
      if (parameters.length) {
        newQueryString += parameters + '&';
      }
    }

    newQueryString += queryParameterName + '=' + encodeURIComponent(queryParameterValue);

    return urlParts[0] + newQueryString + urlFragment;
  }
});

generalModule.factory('setupTrackingId', ['ngEventService', function (ngEventService) {
  return function (trackingId) {
    var oldTrackingId = piano._getTrackingId();
    piano._setTrackingId(trackingId);

    if (oldTrackingId !== trackingId) {
      ngEventService.fire(EVENT_TRACKING_ID_CHANGED, trackingId);
    }
  };
}]);

generalModule.service('utilsService', ['cookieService', function (cookieService) {
  this.isIphone = function () {
    return /iPhone/.test(navigator.userAgent) && !window.MSStream;
  };

  this.isIOS = function () {
    return !!navigator.platform && /iP(ad|hone|od)/.test(navigator.platform);
  };

  this.isFbOrInstagramApp = function () {
    var ua = navigator.userAgent || navigator.vendor || window.opera;
    return (ua.indexOf('FBAN') > -1) || (ua.indexOf('FBAV') > -1) || (ua.indexOf('Instagram') > -1);
  }

  this.isMobileUserAgent = function () {
    return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
  }

  /**
   * Checks if a given value is present in a collection using strict equality
   * for comparisons, i.e. ===
   * @param collection, Array
   * @param target, any
   * @returns boolean
   */
  this.contains = function (collection, target) {
    if (!angular.isArray(collection)) {
      util.log('method contains() works only with Arrays');
      return false;
    }

    return collection.some(function (item) {
      return item === target;
    });
  };

  /**
   * Gets the last element or last n elements of an array.
   * @param collection, array
   * @returns {*}
   */
  this.last = function (collection) {
    if (!angular.isArray(collection)) {
      util.log('method last() works only with Arrays');
      return;
    }

    return collection[collection.length - 1];
  };

  /**
   * Iterates over elements of a collection, executing the callback for each element.
   * Callbacks may exit iteration early by explicitly returning false
   * Objects are iterated like arrays
   * @param collection, array or object
   * @param callback
   */
  this.forEach = function (collection, callback) {
    if (angular.isArray(collection)) {
      collection.forEach(function (item, index) {
        callback(item, index);
      });

      return collection;
    }

    if (angular.isObject(collection)) {
      Object.keys(collection).forEach(function (key) {
        callback(collection[key], key);
      });

      return collection;
    }

    return collection;
  };

  /**
   * Creates an array of values by running each element in the collection through the callback.
   * Objects are iterated like arrays
   * @param collection, array or object
   * @param callback
   * @returns Array
   */
  this.map = function (collection, callback) {
    if (angular.isArray(collection)) {
      return collection.map(function (item, index) {
        return callback(item, index);
      });
    }

    if (angular.isObject(collection)) {
      return Object.keys(collection).map(function (key) {
        return callback(collection[key], key);
      });
    }

    return [];
  };

  /**
   * Reduces a collection to a value which is the accumulated result of running each element
   * in the collection through the callback, where each successive callback execution consumes
   * the return value of the previous execution. If accumulator is not provided the first element
   * of the collection will be used as the initial accumulator value.
   * @param collection
   * @param callback
   * @param accumulator
   */
  this.reduce = function (collection, callback, accumulator) {
    if (!angular.isArray(collection)) {
      return undefined;
    }

    return collection.reduce(callback, accumulator);
  };

  /**
   * Iterates over elements of a collection, returning the first element that
   * the callback returns truey for.
   * @param collection
   * @param callback
   */
  this.find = function (collection, callback) {
    if (!angular.isArray(collection)) {
      util.log('method find() works only with Arrays');
      return;
    }

    return collection.find(callback);
  };

  this.filter = function (collection, callback) {
    if (!angular.isArray(collection)) {
      util.log('method filter() works only with Arrays');
      // return empty array for lodash compability
      return [];
    }

    return collection.filter(callback);
  };

  /**
   * Creates a shallow clone of object excluding the specified properties. Property names may be
   * specified as individual arguments or as arrays of property names. If a callback is provided
   * it will be executed for each property of object omitting the properties the callback returns
   * truey for
   * @param object, Object or Array
   * @param fields, String or Array[string]
   */
  this.omit = function (object, fields) {
    var fieldsToOmit = [];
    var objectToParse = {};

    if (typeof fields === 'string') {
      fieldsToOmit = [fields];
    }

    if (angular.isArray(fields)) {
      fieldsToOmit = [].concat(fields);
    }

    if (angular.isArray(object)) {
      object.forEach(function (value, index) {
        objectToParse[index] = value;
      });
    }

    if (angular.isObject(object) && !angular.isArray(object)) {
      objectToParse = angular.extend({}, object);
    }

    fieldsToOmit.forEach(function (field) {
      delete objectToParse[field];
    });

    return objectToParse;
  };

  this.debounce = function (func, wait, immediate) {
    var timeout;
    return function () {
      var context = this;
      var args = arguments;
      var later = function () {
        timeout = null;
        if (!immediate) func.apply(context, args);
      };
      var callNow = immediate && !timeout;
      clearTimeout(timeout);
      timeout = setTimeout(later, wait);
      if (callNow) func.apply(context, args);
    };
  };

  this.setExtendExpiredAccessCookie = function (aid, value) {
    var expires = new Date();
    expires.setMonth(expires.getMonth() + 6);
    cookieService.setCookie(aid + '__eea', value ? 'true' : 'false', expires.toUTCString(), '/');
  }

  this.removeExtendExpiredAccessCookie = function (aid) {
    cookieService.eraseCookie(aid + '__eea', '/');
  }

  this.jwtDecode = function (token, options) {
    if (!token) {
      return '';
    }

    options = options || {};
    var pos = options.header === true ? 0 : 1;
    return JSON.parse(base64_url_decode(token.split('.')[pos]));
  };

  /**
   * The code was extracted from:
   * https://github.com/davidchambers/Base64.js
   */

  var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';

  function InvalidCharacterError(message) {
    this.message = message;
  }

  InvalidCharacterError.prototype = new Error();
  InvalidCharacterError.prototype.name = 'InvalidCharacterError';

  function polyfill(input) {
    var str = String(input).replace(/=+$/, '');
    if (str.length % 4 === 1) {
      throw new InvalidCharacterError('\'atob\' failed: The string to be decoded is not correctly encoded.');
    }
    for (
      // initialize result and counters
      var bc = 0, bs, buffer, idx = 0, output = '';
      // get next character
      buffer = str.charAt(idx++);
      // character found in table? initialize bit storage and add its ascii value;
      ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,
        // and if not first of each 4 characters,
        // convert the first 8 bits to one ascii character
      bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0
    ) {
      // try to find character in table (0-63, not found => -1)
      buffer = chars.indexOf(buffer);
    }
    return output;
  }

  var atob = window.atob && window.atob.bind(window) || polyfill;

  function b64DecodeUnicode(str) {
    return decodeURIComponent(atob(str).replace(/(.)/g, function (m, p) {
      var code = p.charCodeAt(0).toString(16).toUpperCase();
      if (code.length < 2) {
        code = '0' + code;
      }
      return '%' + code;
    }));
  }

  var base64_url_decode = function (str) {
    var output = str.replace(/-/g, '+').replace(/_/g, '/');
    switch (output.length % 4) {
      case 0:
        break;
      case 2:
        output += '==';
        break;
      case 3:
        output += '=';
        break;
      default:
        throw 'Illegal base64url string!';
    }

    try {
      return b64DecodeUnicode(output);
    } catch (err) {
      return atob(output);
    }
  };

  // https://learn.javascript.ru/task/throttle
  this.throttle = function (func, ms) {
    var isThrottled = false;
    var savedArgs;
    var savedThis;

    function wrapper() {
      if (isThrottled) {
        savedArgs = arguments;
        savedThis = this;
        return;
      }

      func.apply(this, arguments);

      isThrottled = true;

      setTimeout(function () {
        isThrottled = false;
        if (savedArgs) {
          wrapper.apply(savedThis, savedArgs);
          savedArgs = savedThis = null;
        }
      }, ms);
    }

    return wrapper;
  }

  /**
   * Given an array of functions, returns a function that calls all given functions one by one.
   * @param {Array<Function>} functionsToCall
   * @returns {Function}
   */
  this.combine = function (functionsToCall) {
    return function () {
      functionsToCall.forEach(function (func) {
        if (typeof func === 'function') {
          func();
        }
      });
    };
  }

  // from StringUtils.java
  var EMAIL_REGEXP = /^([_A-Za-z0-9-&№^=%'"!?*+./#\\$]+)@([_A-Za-z0-9-&№^=%'"!?*+./#\\]+)\.([_A-Za-z0-9-+.]+)$/;

  // from StringUtils.java
  this.isEmailValid = function (email) {
    var _email = email && email.trim();

    if (!_email) {
      return false;
    }

    if (_email.length > 255) {
      return false;
    }

    return EMAIL_REGEXP.test(_email);
  }

  this.queryStringToMap = function (url) {
    var urlParams = {};
    if (!url) {
      return urlParams;
    }
    var match,
      pl = /\+/g,  // Regex for replacing addition symbol with a space
      search = /([^&=]+)=?([^&]*)/g,
      decode = function (s) {
        return decodeURIComponent(s.replace(pl, " "));
      };
    while (match = search.exec(url)) {
      urlParams[decode(match[1])] = decode(match[2]);
    }
    return urlParams;
  }

  this.getAccountBasedProvidersIDs = function (accountBasedProviders) {
    if (!Array.isArray(accountBasedProviders) || !accountBasedProviders.length) {
      return [];
    }

    return accountBasedProviders.reduce(function (result, provider) {
      if (provider.id !== undefined) {
        result.push(provider.id);
      }

      return result;
    }, []);
  }
}]);

generalModule.factory('getTrackingId', function () {
  return function () {
    return piano._getTrackingId();
  };
});

generalModule.factory('trackExternalEvent', function () {
  return function (eventType, eventGroupId, customParams) {
    piano._logAutoMicroConversion(eventType, eventGroupId, customParams);
  };
});

generalModule.factory('interceptAjax', ['lang', function (lang) {
  /*
   * Override the wait panel
   */
  tinypass.waitLockedBy = null;
  // wait panel can be cause of freezes during user typing.
  tinypass.withoutWaitPanel = false;
  var EVENT_SHOW_WAIT_PANEL = "EVENT_SHOW_WAIT_PANEL";
  var waitEL = null;
  tinypass.showWaitPanel = function (id, content) {
    if (typeof window.CustomEvent === 'function') {
      document.dispatchEvent(new CustomEvent(EVENT_SHOW_WAIT_PANEL, {
        detail: {
          id: id,
          content: content
        }
      }));
    }
  };

  tinypass.hideWaitPanel = function (id) {
    if ((!id && tinypass.waitLockedBy === 'others') || id === tinypass.waitLockedBy) {
      if (waitEL) {
        waitEL.stop();
        waitEL.remove();
      }
      tinypass.waitLockedBy = null;
    }
  };

  return {
    appendLoaderTo: function (className) {
      document.addEventListener(EVENT_SHOW_WAIT_PANEL, function (event) {
        _showWaitPanel(event);
      });

      function _showWaitPanel(event) {
        if (tinypass.withoutWaitPanel || tinypass.waitLockedBy) {
          return;
        }
        tinypass.waitLockedBy = event.detail.id || 'others';
        if (waitEL) {
          waitEL.remove();
        }
        if (!event.detail.content) {
          waitEL = $('<div id="waitParent"><div id="waitPanel">' + lang.tr('Loading...') + '</div></div>');
        } else {
          waitEL = $('<div id="waitParent">' + event.detail.content + '</div>');
        }
        $('.' + className).prepend(waitEL);
        // waitEL.animate({opacity: 1}, 600);
      }
    }
  }
}]);

/**
 * Defines keys based on KeyboardEvent.key (https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key).
 * Complete list of key values can be found here:
 * https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values.
 */
generalModule.factory('keysService', [
  function () {
    var service = {
      isEscape: isEscape,
      isEnter: isEnter,
    };

    return service;

    function isEscape(key) {
      // 'Esc' is IE-specific
      return key === 'Esc' || key === 'Escape';
    }

    function isEnter(key) {
      return key === 'Enter';
    }
  }
]);

generalModule.factory('windowService', ['$window', function ($window) {
  var newWindow;
  var timer;

  return {
    openWindow: openWindow,
  };

  function openWindow(params, onWindowClosedCallback) {
    clearTimer();
    closeWindow();

    newWindow = $window.open(params.url, params.target, params.features);

    timer = setInterval(function () {
      if (!newWindow) {
        clearTimer();
        return;
      }

      if (newWindow.closed) {
        onWindowClosedCallback && onWindowClosedCallback();
        clearTimer();
      }
    }, 1000);

    return newWindow;
  }

  function clearTimer() {
    if (timer) {
      clearInterval(timer);
      timer = null;
    }
  }

  function closeWindow() {
    if (newWindow) {
      newWindow.close();
      newWindow = null;
    }
  }
}]);

generalModule.filter('encodeURIComponent', ['$window', function ($window) {
  return $window.encodeURIComponent;
}]);

var Helper = function () {
};

Helper.wrapMethod = function (funcName, from, to) {
  to[funcName] = function (arg1, arg2, arg3) {
    return from[funcName](arg1, arg2, arg3);
  }
};

var IntegrationEvents = (function () {
  /**
   * Keep track of all available events in system
   * an error will be thrown if someone will try to send
   * event not listed here
   */
  var availableEvents = {
    'externalApiFieldset.change': 'externalApiFieldset.change',
    'ga.created': 'ga.created',
    'ga.performance.tracker.created': 'ga.performance.tracker.created'
  };

  var eventPrefixLocal = '_piano_._local_.';
  var eventPrefixExternal = '_piano_._external_.';

  /**
   * Use that method to send events FROM the angular component
   * to the outside world. Or better use angular wrapper -
   * IntegrationEventsService
   * @param eventName
   * @param data
   */
  function dispatchLocal(eventName, data) {
    throwIfNotAllowed(eventName);

    document.dispatchEvent(new CustomEvent(prefixedLocal(eventName), {detail: immutable(data)}));
  }

  /**
   * Use that method to send events TO the angular component
   * from the outside world.
   * @param eventName
   * @param data
   */
  function dispatchExternal(eventName, data) {
    throwIfNotAllowed(eventName);

    document.dispatchEvent(new CustomEvent(prefixedExternal(eventName), {detail: immutable(data)}));
  }

  /**
   * Listening events sent by angular components
   * @param eventName
   * @param callback
   */
  function listenLocal(eventName, callback) {
    document.addEventListener(prefixedLocal(eventName), function (data) {
      callback(data);
    });
  }

  /**
   * Listening events sent by the custom script / from template
   * @param eventName
   * @param callback
   */
  function listenExternal(eventName, callback) {
    document.addEventListener(prefixedExternal(eventName), function (data) {
      callback(data);
    });
  }

  function prefixedLocal(eventName) {
    return eventPrefixLocal + eventName;
  }

  function prefixedExternal(eventName) {
    return eventPrefixExternal + eventName;
  }

  function throwIfNotAllowed(eventName) {
    if (!(eventName in availableEvents)) {
      throw new Error('event ' + eventName + ' is not recognized');
    }
  }

  function immutable(data) {
    var isDate = angular.isDate(data);
    var isArray = angular.isArray(data);
    var isObject = angular.isObject(data);

    if (isDate) {
      return new Date(data);
    }

    if (isArray) {
      return angular.copy(data, []);
    }

    if (isObject && !isArray) {
      return angular.copy(data, {});
    }

    return data;
  }

  return {
    dispatchLocal: dispatchLocal,
    dispatchExternal: dispatchExternal,
    listenLocal: listenLocal,
    listenExternal: listenExternal
  };
}());

// https://tc39.github.io/ecma262/#sec-array.prototype.findIndex
if (!Array.prototype.findIndex) {
  Object.defineProperty(Array.prototype, 'findIndex', {
    value: function (predicate) {
      // 1. Let O be ? ToObject(this value).
      if (this == null) {
        throw new TypeError('"this" is null or not defined');
      }

      var o = Object(this);

      // 2. Let len be ? ToLength(? Get(O, "length")).
      var len = o.length >>> 0;

      // 3. If IsCallable(predicate) is false, throw a TypeError exception.
      if (typeof predicate !== 'function') {
        throw new TypeError('predicate must be a function');
      }

      // 4. If thisArg was supplied, let T be thisArg; else let T be undefined.
      var thisArg = arguments[1];

      // 5. Let k be 0.
      var k = 0;

      // 6. Repeat, while k < len
      while (k < len) {
        // a. Let Pk be ! ToString(k).
        // b. Let kValue be ? Get(O, Pk).
        // c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)).
        // d. If testResult is true, return k.
        var kValue = o[k];
        if (predicate.call(thisArg, kValue, k, o)) {
          return k;
        }
        // e. Increase k by 1.
        k++;
      }

      // 7. Return -1.
      return -1;
    }
  });
}

// Piano API available in iframe global scope
var piano = (function (integrationEvents) {
  var self = {};
  var module = {};

  /**
   * Listen events that are sent from angular
   * this is for integration purpose - to allow custom
   * scripts in the templates listen to events we
   * want them to be able to listen
   * @param eventName, see all available events in IntegrationEvents, add yours if you wish
   * @param callback
   */
  module.listen = function (eventName, callback) {
    integrationEvents.listenLocal(eventName, callback);
  };

  /**
   * Send an event to angular environment
   * (event from custom scripts)
   * this is for integration purpose - to allow custom
   * scripts in the templates communicate with Angular
   * components through our event API (we need to listen for a given
   * event in the component)
   * @param eventName, see all available events in IntegrationEvents, add yours if you wish
   * @param data, any data
   */
  module.externalEvent = function (eventName, data) {
    integrationEvents.dispatchExternal(eventName, data);
  };

  module._setTrackingId = function (trackingId) {
    self.trackingId = trackingId;
  };

  module._getTrackingId = function () {
    return self.trackingId;
  };

  var doLog = function (url, params) {
    if (!self.trackingId) {
      return;
    }
    var os = ((TPParam || {}).params || {}).os || 'desktop';
    switch (os) {
      case 'ios':
      case 'android':
        doLogForMobileSDK(url, params);
        break;
      default:
        parent.postMessage(JSON.stringify({event: 'iframeLogRequest', url: url, queryParams: params}), '*')
    }
  };

  var doLogForMobileSDK = function (url, params) {
    var queryParams = [];
    for (var key in params) {
      var value = params[key];
      queryParams.push(encodeURIComponent(key) + '=' + encodeURIComponent(typeof value === 'object' ? JSON.stringify(value) : value));
    }

    queryParams = queryParams.join('&');
    var queryString = url + '?' + queryParams;
    if (!util.debug && navigator.sendBeacon) {
      navigator.sendBeacon(queryString);
    } else {
      var request = new XMLHttpRequest();
      request.open('GET', queryString, false);
      request.send();
    }
  };

  module._logAutoMicroConversion = function (eventType, eventGroupId, customParams) {
    var logEventParams = {
      tracking_id: self.trackingId,
      event_type: eventType,
      event_group_id: eventGroupId,
      custom_params: (customParams || "")
    };

    if (typeof eventGroupId === 'undefined') {
      util.log('Parameter eventGroupId is required to log micro conversion.')
      return;
    }

    if (typeof eventType === 'undefined') {
      util.log('Parameter eventType is required to log micro conversion.')
      return;
    }

    doLog('/api/v3/conversion/logAutoMicroConversion', logEventParams)
  };

  /**
   * Send request about funnel step
   * All this function have duplicate in tp.log object
   * @param stepNumber
   * @param stepName
   * @param customParams
   */
  module.logFunnelStep = function (stepNumber, stepName, customParams) {
    var logEventParams = {
      tracking_id: self.trackingId,
      step_number: stepNumber,
      step_name: stepName,
      custom_params: (customParams || "")
    };

    if (typeof stepNumber === 'undefined') {
      util.log('Parameter stepNumber is required to log funnel step.')
      return;
    }
    if (typeof stepName === 'undefined') {
      util.log('Parameter stepName is required to log funnel step.')
      return;
    }

    doLog('/api/v3/conversion/logFunnelStep', logEventParams)
  };

  /**
   * Send request about micro conversion
   * This function have duplicate in tp.log object
   * @param eventGroupId
   * @param customParams
   */
  module.logMicroConversion = function (eventGroupId, customParams) {
    var logEventParams = {
      tracking_id: self.trackingId,
      event_group_id: eventGroupId,
      custom_params: (customParams || "")
    };

    if (typeof eventGroupId === 'undefined') {
      util.log('Parameter eventGroupId is required to log micro conversion.')
      return;
    }

    doLog('/api/v3/conversion/logMicroConversion', logEventParams)
  };

  /**
   * Send request about conversion
   * This function have duplicate in tp.log object
   * @param termId
   * @param termName
   * @param stepNumber
   * @param amount
   * @param currency
   * @param customParams
   * @param conversionCategory
   */
  var doLogConversion = function (termId, termName, stepNumber, amount, currency, customParams, conversionCategory) {
    var logParams = {
      tracking_id: self.trackingId,
      term_id: termId,
      term_name: termName
    };

    if (typeof termId === 'undefined') {
      util.log('Parameter termId is required to log conversion.')
      return;
    }
    if (typeof termName === 'undefined') {
      util.log('Parameter termName is required to log conversion.')
      return;
    }
    if (typeof stepNumber !== 'undefined') {
      logParams.step_number = stepNumber;
    }
    if (typeof amount !== 'undefined') {
      logParams.amount = amount;
    }
    if (typeof currency !== 'undefined') {
      logParams.currency = currency;
    }
    if (typeof customParams !== 'undefined') {
      logParams.custom_params = customParams;
    }
    if (typeof conversionCategory !== 'undefined') {
      logParams.conversion_category = conversionCategory;
    }

    doLog('/api/v3/conversion/log', logParams)
  };

  /**
   * Send request about conversion
   * This function have duplicate in tp.log object
   */
  module.logConversion = function (params) {
    if (typeof params === 'object') {
      doLogConversion(
        params.term_id,
        params.term_name,
        params.step_number,
        params.amount,
        params.currency,
        params.custom_params,
        params.conversion_category
      );
    } else {
      doLogConversion.apply(this, arguments);
    }
  };

  /**
   * Set user into template and reload content
   * @param userToken
   */
  module.reloadTemplateWithUserToken = function (userToken) {
  };

  return module;
}(IntegrationEvents));

var eventModule = angular.module('eventModule', ['generalModule']);

eventModule.factory('eventService', ['$window', '$rootScope', '$q', 'configService', function ($window, $rootScope, $q, configService) {

  var es = {};

  es.last = null;
  es.resultCallbacks = {};

  /**
   * Post a message to the parent frame
   * @param eventName
   * @param eventData
   */
  es.postMessage = function (eventName, eventData) {
    var os = ((TPParam || {}).params || {}).os || 'desktop';
    try {
      switch (os) {
        case 'ios':
          iosMessage(eventName, eventData);
          break;
        case 'android':
          androidMessage(eventName, eventData);
          break;
        default:
          desktopMessage(eventName, eventData);
      }
    } catch (e) {
      util.log("[EVENT SERVICE] Event wasn't expected: ", eventName);
    }
  };

  es.customPostMessage = function (eventName, eventData, origin, recipient) {
    var message = {};
    message.sender = getIFrameId();
    message.event = eventName;
    message.params = eventData;

    util.log("[EVENT SERVICE] Custom post message:" + eventName, message);
    $.postMessage(JSON.stringify(message), origin, recipient);
  };

  /**
   * Resize event
   * @param params
   */
  es.resizeEvent = function (params) {
    params.iframeId = getIFrameId();
    if (es.last === null) {
      es.postMessage('loaded', params);
      es.last = params;
    } else if (params.height !== es.last.height || params.width !== es.last.width) {
      var closeButtonTypeParam = configService.get().closeButtonType;

      if (closeButtonTypeParam !== undefined) {
        params.closeButtonType = closeButtonTypeParam;
      }

      es.postMessage('resize', params);
      es.last = params;
    }
  };

  /**
   * Purge event
   * @param params
   */
  es.purgeEvent = function (params) {
    es.postMessage('purge', params);
  };

  /**
   * Close event
   * @param params
   */
  es.closeEvent = function (params) {
    es.postMessage('close', params);
  };

  /**
   * Close and refresh event
   * @param params
   */
  es.closeAndRefreshEvent = function (params) {
    es.postMessage('closeAndRefresh', params);
  };

  /**
   * Fires after changing of the state
   * For example: After selecting a term in the offer
   * @param stateName
   * @param termObject
   */
  es.checkoutStateChange = function (stateName, termObject) {
    es.postMessage('checkoutStateChange',
      { stateName: stateName, offerId: TPParam.config.offerId, term: termObject });
  };

  /**
   *
   * @param params
   */
  es.startCheckoutEvent = function (params) {
    es.postMessage('startCheckout', params);
  };

  /**
   * Fires after initialization of the bank secure state
   */
  es.startThreeDBankSecure = function () {
    es.postMessage('startThreeDBankSecure');
  };

  /**
   * Fire a custom event - used for external integration
   * @param params
   */
  es.loginRequiredEvent = function (params) {
    var promiser = $q.defer();
    params.resultCallback = function (object) {
      promiser.resolve(object)
    };

    es.postMessage('loginRequired', params);
    return promiser.promise;
  };

  es.initContext = function () {
    var promiser = $q.defer();
    es.postMessage('initContext', {
      resultCallback: function (object) {
        promiser.resolve(object)
      }
    });
    return promiser.promise;
  };

  es.gigyaPasswordlessLoginEvent = function (params) {
    params.iframeId = getIFrameId();
    es.postMessage('gigyaPasswordlessLogin', params);
  };

  /**
   * Fire a custom event - used for external integration
   * @param params
   * @param name
   */
  es.customEvent = function (name, params) {
    es.postMessage(name, params);
  };

  /**
   * System error event.  Checkout process will stop
   */
  es.systemErrorEvent = function (params) {
    es.postMessage('error', params);
  };

  /**
   * Fire when users click ‘submit payment’
   */
  es.submitPayment = function (params) {
    es.postMessage('submitPayment', params);
  };

  /**
   * Fire when the checkout process has completed
   */
  es.completeEvent = function (params) {
    es.postMessage('complete', params);
  };

  /**
   * Fire when user already has access to resource
   */
  es.alreadyPurchasedEvent = function (params) {
    es.postMessage('alreadyPurchased', params);
  };

  /**
   * Fire when the checkout payment has error
   */
  es.checkoutPaymentErrorEvent = function (message) {
    es.postMessage('checkoutPaymentError', { message: message });
  };

  /**
   * Fire in mobile devices after login event click
   */
  es.loginEvent = function (params) {
    es.postMessage('login', params);
  };

  /**
   * Fire in mobile devices after register event click
   */
  es.registerEvent = function (params) {
    es.postMessage('register', params);
  };

  /**
   * Fire to request parent window sizes (width, height).
   */
  es.getParentSize = function () {
    es.postMessage('parentSize');
  };

  es.isMobileDevice = function () {
    return TPParam && TPParam.params && (TPParam.params.os === 'ios' || TPParam.params.os === 'android');
  };

  es.emitGAEvent = function (eventParams) {
    es.postMessage('emitGAEvent', eventParams);
  };

  es.emitMetricsEvent = function (eventParams) {
    es.postMessage('emitMetricsEvent', eventParams);
  };

  $().ready(function() {
    $.receiveMessage(function (event) {
      var postMessageParams;
      var ignoredOrigins = ["https://core.spreedly.com", "https://jsl.prod.obi.aol.com", "https://pay.datatrans.com",
        "https://vpos.infonet.com.py:8888", "https://vpos.infonet.com.py", "https://flex.cybersource.com", "https://testflex.cybersource.com"];//non-JSON messages
      try {
        util.log("[EVENT SERVICE] Received message:", event);

        if (ignoredOrigins.indexOf(event.origin) !== -1) {
          return;
        }

        // remove comments from the event data, otherwise JSON.parse method fires error during the parse some events
        var eventData = event.data.replace(/\/\*.*?\*\//g, '');
        postMessageParams = JSON.parse(eventData);

        if (postMessageParams.sender && postMessageParams.sender.indexOf('piano-id-') === 0) {
          postMessageParams.event = postMessageParams.sender + '-' + postMessageParams.event;
        }
        if(postMessageParams.eventName === 'init-social'){
          postMessageParams = {
            event: 'init-social',
            params: postMessageParams
          };
        }
        if (postMessageParams.event && postMessageParams.event.indexOf('-3ds-complete') !== -1) {
          postMessageParams = {
            event: '3ds-complete',
            params: postMessageParams
          };
        }

        $rootScope.$broadcast(postMessageParams.event, postMessageParams.params);
        if (postMessageParams.params && postMessageParams.params.resultCallbackId) {
          if (es.resultCallbacks[postMessageParams.params.resultCallbackId]) {
            es.resultCallbacks[postMessageParams.params.resultCallbackId](postMessageParams.params);
            delete es.resultCallbacks[postMessageParams.params.resultCallbackId];
          }
        }
      } catch (err) {
        util.log("[EVENT SERVICE][Could not parse message:", event);
        return;
      }
    });
  });

  function iosMessage(eventName, eventData) {
    window.webkit.messageHandlers[eventName].postMessage(eventData);
  }

  function androidMessage(eventName, eventData) {
    var convertedData = JSON.stringify(eventData);
    PianoAndroid[eventName](convertedData);
  }

  function getIFrameId() {
    return getParameterByName('iframeId');
  }

  function desktopMessage(eventName, eventData) {
    var message = {};
    es.parentURL = getParameterByName('url');
    if (!es.parentURL) {
      es.parentURL = (window.location !== window.parent.location) ? document.referrer : document.location.href;
    }
    message.sender = getIFrameId();
    message.displayMode = getParameterByName('displayMode');
    message.recipient = "opener";
    message.event = eventName;
    message.params = eventData;

    if (eventData && eventData.resultCallback) {
      var resultEventId = _randomString(16);
      message.params.resultCallbackId = resultEventId;
      es.resultCallbacks[resultEventId] = eventData.resultCallback;

      delete eventData["resultCallback"];
    }

    util.log("[EVENT SERVICE] Post message:" + eventName, message);
    $.postMessage(JSON.stringify(message), es.parentURL, message.displayMode === 'popup' ? $window.opener : $window.parent);
  }

  return es;
}]);

var errorModule = angular.module('errorModule', ['generalModule']);

errorModule.factory('errorService', ['$rootScope', 'eventService', 'ngEventService',
  function ($rootScope, eventService, ngEventService) {
    /**
     *  This is error map by errorInstanceId
     *  Look like following structure:
     *  {
     *    'errorInstanceId' : {
     *          errors : [
     *             {
     *                type: "global", "component", "terminal" was displayed
     *                field: "field", - optional parameter
     *                fieldTitle: "user frendle field name", - optional parameter
     *                message: "error message",
     *                displayed: true/false  error was displayed
     *             }
     *         ]
     *       childs : [
     *         errors: []
     *         childs: []
     *       ],
     *       errorInstanceId : 'errorInstanceId',
     *       $id: "DD" // scope $id.
     *    }
     *  }
     */
    var errors = {};

    var _isScope = function (obj) {
      return obj && obj.$evalAsync && obj.$watch;
    };

    var _TYPES = {
      GLOBAL: "global",
      COMPONENT: "component",
      TERMINAL: "terminal"
    };

    return function ($scope) {
      var me = {};
      var myErrors = null;

      var init = function () {
        var parentErrorInstanceId, getParentErrorInstanceId;

        if ($scope) {
          if (!_isScope($scope)) {
            throw Error("The first parameter should be scope");
          }

          if (!$scope.errorInstanceId) {
            $scope.errorInstanceId = _randomString(16);
            myErrors = { children: [], errors: [], errorInstanceId: $scope.errorInstanceId, $id: $scope.$id };
            errors[$scope.errorInstanceId] = myErrors;

            getParentErrorInstanceId = function (sc) {
              if (!sc) {
                return null;
              }

              if (sc.errorInstanceId) {
                return sc.errorInstanceId;
              }

              if (sc.$parent) {
                return getParentErrorInstanceId(sc.$parent);
              }
            };

            parentErrorInstanceId = getParentErrorInstanceId($scope.$parent);

            if (parentErrorInstanceId) {
              var parentScope = errors[parentErrorInstanceId];
              parentScope.children.push(myErrors);
            }
          } else {
            myErrors = errors[$scope.errorInstanceId];
          }
        }
      };

      me.error = function (type, error) {
        if (!$scope) {
          throw Error("The $scope is undefined");
        }

        var _error = error;

        if (!_error) {
          _error = {}
        }

        if (angular.isString(_error)) {

          _error = {
            message: error
          }
        }

        _error.type = type;
        _error.errorInstanceId = $scope.errorInstanceId;

        myErrors.errors.push(_error);

        ngEventService.fire(EVENT_ERRORS_CHANGED);
      };

      me.global = function (error) {
        me.error(_TYPES.GLOBAL, error);
      };

      me.globals = function (errorsList) {
        angular.forEach(errorsList, function (error) {
          me.error(_TYPES.GLOBAL, error);
        });
      };

      me.component = function (error) {
        me.error(_TYPES.COMPONENT, error);
      };

      me.components = function (errorsList) {
        angular.forEach(errorsList, function (error) {
          me.error(_TYPES.COMPONENT, error);
        });
      };

      me.terminal = function (error) {
        var _error = error;
        if (!_error) {
          _error = {}
        }

        if (angular.isString(_error)) {

          _error = {
            message: error
          }
        }

        _error.type = _TYPES.TERMINAL;
        if ($scope) {
          _error.errorInstanceId = $scope.errorInstanceId;
        }

        ngEventService.fire(EVENT_TERMINAL_ERROR, _error);

        $rootScope.terminalError = _error.message;
        $rootScope.terminalErrorHeading = _error.heading;
        eventService.systemErrorEvent({ message: _error.message });
      };

      me.reset = function () {
        if ($scope) {
          if (myErrors.errors.length > 0) {
            myErrors.errors = [];
            ngEventService.fire(EVENT_ERRORS_CHANGED, $scope.errorInstanceId);
          }
        }
      };

      me.errors = function (withoutChildren) {
        var result = [];
        if ($scope) {
          var getErrors = function (errorInstance) {
            angular.forEach(errorInstance.errors, function (error) {
              result.push(error)
            });
            if (!withoutChildren) {
              angular.forEach(errorInstance.children, function (child) {
                getErrors(child)
              })
            }
          };
          getErrors(myErrors)
        } else {
          angular.forEach(errors, function (errorInstance) {
            angular.forEach(errorInstance.errors, function (error) {
              result.push(error)
            });
          });
        }

        return result;
      };
      me.childrenErrors = function () {
        var result = [];
        if ($scope) {
          var getErrors = function (errorInstance) {
            angular.forEach(errorInstance.errors, function (error) {
              result.push(error)
            });
            angular.forEach(errorInstance.children, function (child) {
              getErrors(child)
            })
          };
          angular.forEach(myErrors.children, function (child) {
            getErrors(child)
          });
        } else {
          result = [];
        }

        return result;
      };

      me.resetDisplayed = function () {
        var id = me.getId();
        angular.forEach(errors, function (errorInstance) {
          var _errors = [];
          angular.forEach(errorInstance.errors, function (error) {
            if (error.displayedOn !== id) {
              _errors.push(error)
            }
          });

          errorInstance.errors = _errors
        });

        ngEventService.fire(EVENT_ERRORS_CHANGED, $scope.errorInstanceId);
      };

      me.TYPES = _TYPES;
      me.getId = function () {
        if ($scope) {
          return $scope.errorInstanceId;
        }
        return null;
      };

      init();
      return me;
    }
  }]);

var tpComponentsModule = angular.module('tpComponentsModule', ['generalModule', 'containerServiceModule']);

/**
 * All links that href to an external URL will be set to target="_blank"
 * _parent is also permitted.  All other values will be defaulted to _blank.
 *
 * Registers "external_link" event when user follows external link.
 */
tpComponentsModule.directive('a', ['trackExternalEvent', 'getTrackingId', 'addQueryParameterToUrl', '$timeout',
  'ngEventService',
  function (trackExternalEvent, getTrackingId, addQueryParameterToUrl, $timeout, ngEventService) {
    function getURLToTrack(href) {
      var urlToTrack = href.split(/[?#]/)[0];
      var lastCharacterNumber = urlToTrack.length - 1;
      if (urlToTrack[lastCharacterNumber] === '/') {
        urlToTrack = urlToTrack.slice(0, lastCharacterNumber)
      }
      return urlToTrack;
    }

    return {
      restrict: 'E',
      link: function (scope, element) {
        var unsubTrackingIdChanged;
        $timeout(function () {
          var elAttr = element.attr("href");
          if (elAttr && elAttr.indexOf("http") === 0) {
            //The only allowed targets are _parent and _blank. _blank is the default
            if (element.attr("target") !== "_parent" && element.attr("target") !== "_top") {
              element.attr("target", "_blank");
            }
            addTrackingIdToHref();
            unsubTrackingIdChanged = ngEventService.subscribe(EVENT_TRACKING_ID_CHANGED, function () {
              $timeout(function () {
                addTrackingIdToHref();
              });
            });
          }

          function addTrackingIdToHref() {
            var needTrackingId = element.attr("noptid") === undefined;
            var trackingId = getTrackingId();
            if (needTrackingId && trackingId) {
              // add trackingId into external links
              element.attr('href', addQueryParameterToUrl(element.attr('href'), '_ptid', trackingId));
            }
          }
        });

        element.on('click', function (event) {
          if (event.type === 'click') {
            var href = element.attr("href") || "";

            if (href.lastIndexOf("http", 0) === 0) {
              // href starts with "http" => track

              trackExternalEvent('EXTERNAL_LINK', decodeURI(getURLToTrack(href)), {
                'href': encodeURI(href)
              });
            }
          }
        });

        scope.$on('$destroy', function () {
          unsubTrackingIdChanged && unsubTrackingIdChanged();
        });
      }
    };
  }]);

tpComponentsModule.directive('radioTrackBy', function () {
  return {
    restrict: "A",
    scope: {
      ngModel: "=",
      ngValue: "=",
      radioTrackBy: "@"
    },
    link: function (ng) {
      if (ng.ngValue[ng.radioTrackBy] === ng.ngModel[ng.radioTrackBy]) {
        ng.ngModel = ng.ngValue;
      }
    }
  };
});

tpComponentsModule.factory('getExternalEventParams', function () {
  function lowerCaseFirstLetter(string) {
    return string.charAt(0).toLowerCase() + string.slice(1);
  }

  function getExternalEventParams(attrs) {
    var params = {};
    for (var attrName in attrs) {
      if (attrs.hasOwnProperty(attrName) && attrName.indexOf('externalEvent') === 0 && attrName !== 'externalEvent') {
        params[lowerCaseFirstLetter(attrName.split('externalEvent')[1])] = attrs[attrName];
      }
    }
    return params;
  }

  return getExternalEventParams;
});

/**
 * Send "customEvent" if click on element with directive.
 */
tpComponentsModule.directive('externalEvent', ['eventService', 'getExternalEventParams', 'trackExternalEvent',
  function (eventService, getExternalEventParams, trackExternalEvent) {
    return {
      restrict: 'A',
      link: function (scope, element, attrs) {
        element.on('submit click', function (event) {
          if (~['click', 'submit'].indexOf(event.type)) {
            // track external event
            var externalEventParams = getExternalEventParams(attrs);
            trackExternalEvent('EXTERNAL_EVENT', attrs.externalEvent, externalEventParams);

            eventService.customEvent('customEvent', {
              eventName: attrs.externalEvent,
              params: getExternalEventParams(attrs)
            });
          }
        });
      }
    };
  }]);

/**
 * This directive will advance the checkout process to the next view
 */
tpComponentsModule.directive('showIf', ['$animate', function ($animate) {
  return {
    restrict: 'A',
    link: function (scope, element, attr) {
      scope.$watch(attr.showIf, function ngShowWatchAction(value) {
        element.html(toBoolean(value) ? value : '');
        element.toggleClass('ng-hide', toBoolean(value));
      });
    }
  };
}]);

/**
 * @returns {Boolean} Returns 'true' if 'mobile device', 'false' if 'other device'.
 */
tpComponentsModule.factory('checkMobile', ['$q', '$timeout', 'containerService',
  function ($q, $timeout, containerService) {
    return function () {
      var width = containerService.getNewWidth();
      return width <= 600;
    }
  }]);

/**
 * Show content if mobile device
 */
tpComponentsModule.directive('mobile', ['checkMobile', function (checkMobile) {
  return {
    restrict: 'A',
    transclude: true,
    scope: {},
    template: '<div class="mobile-mode" ng-if="check()" ng-transclude></div>',
    controller: ["$scope", function (scope) {
      scope.check = function () {
        return checkMobile()
      };
    }]
  };
}]);

/**
 * Show content if not mobile device
 */
tpComponentsModule.directive('desktop', ['checkMobile', function (checkMobile) {
  return {
    restrict: 'A',
    transclude: true,
    scope: {},
    template: '<div class="desktop-mode" ng-if="check()" ng-transclude></div>',
    controller: ["$scope", function (scope) {
      scope.check = function () {
        return !checkMobile()
      };
    }]
  };
}]);

/**
 * Run your function on keyup enter key
 */
tpComponentsModule.directive("onEnter", ['$parse', function ($parse) {
  return function (scope, element, attr) {
    var fn = $parse(attr['onEnter']);
    element.bind('keyup', function (event) {
      if (event.keyCode === 13) {
        scope.$apply(function () {
          fn(scope, { $event: event });
        });
      }
    });
  }
}]);

tpComponentsModule.directive("errorMessage", ['$parse', function ($parse) {
  return {
    scope: false,
    controller: ['$scope', '$element', '$attrs', 'ngEventService', 'errorService', 'shakeService',
      function ($scope, $element, $attrs, ngEventService, errorService, shakeService) {
        var types = errorService($scope).TYPES;
        var errorInstanceId = errorService($scope).getId();
        var field = $attrs['name'];
        var style = $attrs['errorMessage'];
        var errorEl = null;

        var clearErrors = function () {
          if (errorEl) {
            errorEl.remove();
          }
          $element.removeClass('error');
        };

        var unsubErrorsChanged = ngEventService.subscribe(EVENT_ERRORS_CHANGED, function () {
          clearErrors();
          var myError = null;
          var errors = errorService($scope).errors();

          angular.forEach(errors, function (error) {
            if (angular.isObject(error) && error.field === field && error.type === types.COMPONENT) {
              myError = error;
            }
          });

          if ($element.is(":visible") && myError && (!myError.displayed || myError.displayedOn === errorInstanceId)) {
            myError.displayed = true;

            if (style === "shake") {
              shakeService.shake($element);
              $element.addClass('error');
            } else {
              errorEl = $("<div class='" + style + "'>" + myError.message + "</div>");
              $element.after(errorEl);
              $element.addClass('error');
            }
          }
        });

        $element.click(function () {
          clearErrors();
        });

        $scope.$on('$destroy', function () {
          unsubErrorsChanged();
        });
      }]
  }
}]);

/**
 * Directive to set modal width from template
 */
tpComponentsModule.directive('config', ['containerService', 'configService',
  function (containerService, configService) {
    return {
      restrict: 'E',
      link: function (scope, element, $attrs) {
        if (isNumber($attrs.width)) {
          containerService.setConfigWidth($attrs.width);
          containerService.resize();
        }

        var hasProperties = false;

        angular.forEach($attrs, function (value, name) {
          if (!startsWith(name, '$')) {
            hasProperties = true;
            configService.setProperty(name, value);
          }
        });

        if (hasProperties) {
          configService.changed();
        }

        function startsWith(str, prefix) {
          return (str.substr(0, prefix.length) === prefix);
        }
      }
    };
  }]);

/**
 * Directive to hide 'no-child' blocks, emulate css selector :black
 */

tpComponentsModule.directive('hideIfBlank', ['$animate',
  function ($animate) {
    return {
      link: function (scope, element) {
        scope.$watch(function () {
        }, function () {
          var usefulText = element.text().replace(/[\s\uFEFF\xA0\n]/gim, '');
          var isBlank = element.children().length === 0 && usefulText === '';
          element.toggleClass('ng-hide', isBlank);
        });
      }
    };
  }
]);

tpComponentsModule.directive('reCaptcha', ['ngEventService', 'checkMobile', 'containerService',
  function (ngEventService, checkMobile, containerService) {

    var attemptsToRender = 3;
    var id;
    var resetSubscription;

    return {
      restrict: 'EA',
      template: "",
      scope: {
        key: '@',
        showResetButton: '=?',
        onResolve: '&?',
        onReady: '&?',
        onExpired: '&?'
      },
      link: link
    };

    function link($scope, $element) {
      var isMobile = false;

      if (!$scope.key) {
        console.warn('There is no re-captcha site-key. See more: https://developers.google.com/recaptcha/docs/display');
        return;
      }

      init();

      $scope.resolve = resolve;

      function init() {
        render(attemptsToRender);
        isMobileDevice();
        resetSubscription = subscribeToCaptchaReset();
      }

      $element.on('$destroy', resetSubscription);

      function render(attemps) {

        if (attemps === 0) {
          console.warn("ReCaptcha can't be initialized");
          return;
        }

        try {
          id = grecaptcha.render($element.get(0), {
            'sitekey': $scope.key,
            'callback': resolve,
            'expired-callback': function () {
              $scope.onExpired && $scope.onExpired();
            }
          });

          if ($scope.showResetButton) {
            $element.addClass('re-captcha--right-padded');
            addResetButton(id);
          }

          $(window).on('resize', _.debounce(onIframeResize, 300));

          if ($scope.onReady) {
            $scope.onReady({ id: id });
          }

        } catch (e) {
          setTimeout(function () {
            render(attemps--);
          }, 1000);
        }
      }

      function subscribeToCaptchaReset() {
        return ngEventService.subscribe('RECAPTCHA_RESET', function () {
          if (grecaptcha && grecaptcha.reset) {
            grecaptcha.reset(id);
          }
        });
      }

      function onIframeResize() {
        isMobileDevice();
        resizeCaptcha();
      }

      function resizeCaptcha() {
        var isWideScreen = containerService.getNewWidth() > 320;

        if (!isMobile) {
          $element.removeClass('re-captcha--mobile re-captcha--mobile-wide');
          return;
        }

        if (isWideScreen) {
          $element.addClass('re-captcha--mobile-wide');
        } else {
          $element.removeClass('re-captcha--mobile-wide');
        }

        $element.addClass('re-captcha--mobile');

      }

      function isMobileDevice() {
        isMobile = checkMobile();
      }

      function addResetButton(id) {
        if (grecaptcha && grecaptcha.reset) {
          var refreshButtonElement = document.createElement('div');
          refreshButtonElement.className = 'reset-icon pn-icon--refresh';
          refreshButtonElement.onclick = function () {
            grecaptcha.reset(id);
            addResetButton(id);
          };

          appendResetElement(refreshButtonElement);
        }
      }

      function appendResetElement(element) {
        if (!angular.element('.reset-icon.pn-icon--refresh').length) {
          $element.append(element);
        }
      }

      function resolve(response) {
        $element.addClass('captcha-resolved');

        if (!$scope.onResolve) {
          return;
        }

        $scope.$apply(function () {
          $scope.onResolve({ response: response });
        });
      }
    }
  }]);

tpComponentsModule.directive('ngEnter', function () {
  return function (scope, element, attrs) {
    element.bind("keydown keypress", function (event) {
      if (event.which === 13) {
        scope.$apply(function () {
          //noinspection JSUnresolvedVariable
          scope.$eval(attrs.ngEnter);
        });

        event.preventDefault();
      }
    });
  };
})

var userServiceModule = angular.module('userServiceModule', ['ui.router', 'ajaxServices', 'ngSanitize', 'generalModule', 'tp.i18n']);

userServiceModule.factory('userService', [
  '$rootScope', '$rootElement', '$q', 'tpHTTP', 'eventService',
  'ngEventService', 'errorService', 'gaService', 'lang', 'utilsService', 'cookieService',
  function (
          $rootScope, $rootElement, $q, tpHTTP, eventService,
          ngEventService, errorService, gaService, lang, utilsService, cookieService
  ) {
    var loginWindowId = _randomString(16);
    var loginWindow = null;
    var loginSuccessPosted = false;
    var loginSuccessCallbacks = [];

    var configProvider = function () {
      return {
        app: {},
        user: {},
        stats: {},
        // fills params that is passed to login iframe on creation in URL
        fillLoginIframeParams: emptyFn,

        // fills params for login required event to submit to Google Analytics
        fillLoginRequiredParamsToGoogleAnalytics: emptyFn,

        // fills params for login required event to submit to publisher's web site
        fillLoginRequiredEventParams: emptyFn,

        // fills params for login success event to submit to publisher's web site
        fillLoginSuccessEventParams: emptyFn
      }
    }

    var userService = {};

    userService.$rootScope = $rootScope;
    userService.rootElement = $rootElement;

    var emptyFn = function () {
    };

    userService.getConfig = function () {
      return {
        app: configProvider().app,
        user: configProvider().user,
        stats: configProvider().stats,

        // fills params that is passed to login iframe on creation in URL
        fillLoginIframeParams: configProvider().fillLoginIframeParams || emptyFn,

        // fills params for login required event to submit to Google Analytics
        fillLoginRequiredParamsToGoogleAnalytics: configProvider().fillLoginRequiredParamsToGoogleAnalytics || emptyFn,

        // fills params for login required event to submit to publisher's web site
        fillLoginRequiredEventParams: configProvider().fillLoginRequiredEventParams || emptyFn,

        // fills params for login success event to submit to publisher's web site
        fillLoginSuccessEventParams: configProvider().fillLoginSuccessEventParams || emptyFn
      };
    };

    userService.init = function (getConfigFn) {
      configProvider = getConfigFn;

      if (!TPParam.LOGIN) {
        throw "Login handler URL (variable TPParam.LOGIN) is not set";
      }
    };

    /**
     * Will return true is we have a valid logged in user
     */
    userService.isUserValid = function () {
      return userService.getConfig().user && userService.getConfig().user.valid === true;
    };

    userService.getUserUid = function () {
      return userService.getConfig().user && userService.getConfig().user.uid;
    };

    /**
     * Will return true if current date greater than last login time plus token expire time
     * Last login time will be set only if "extend expires access" is enabled
     */
    userService.isUserReadOnly = function () {
      if (this.isPianoIdUserProvider()) {
        return false;
      }

      var eea = cookieService.getCookie(userService.getConfig().app.aid + '__eea');
      var user = userService.getConfig().user;
      return eea !== 'true' ? false : Date.now() > (parseInt(user.login_timestamp, 10) + user.token_expiration);
    };

    userService.isUserConfirmed = function () {
      return userService.getConfig().user && userService.getConfig().user.confirmed !== false;
    };

    userService.allowLogout = function () {
      return userService.isUserValid() && userService.isProviderAllowLogout();
    };

    userService.allowLogin = function () {
      return userService.allowTinypassAccountsLogin() || userService.isGigyaUserProvider();
    };

    userService.allowTinypassAccountsLogin = function () {
      var app = userService.getConfig().app;
      return app && app.useTinypassAccounts;
    };

    userService.isPublisherUserRefProvider = function () {
      return getUserProvider() === 'publisher_user_ref';
    };

    userService.isGigyaUserProvider = function () {
      return getUserProvider() === 'gigya';
    };
    userService.isCondeUserProvider = function () {
      return getUserProvider() === 'conde';
    };
    userService.isPianoIdUserProvider = function () {
      return getUserProvider() === 'piano_id';
    };
    userService.isPianoIdLiteUserProvider = function () {
      return getUserProvider() === 'piano_id_lite';
    };
    userService.isJanrainUserProvider = function () {
      return getUserProvider() === 'janrain';
    };

    function getUserProvider() {
      var source = window.TPParam.params || window.TPParam.app || userService.getConfig().app;
      return source ? source.userProvider : '';
    }

    userService.allowMyAccountLogin = function () {
      return userService.allowTinypassAccountsLogin();
    };

    userService.allowTinypassAccountsLogout = function () {
      return userService.isProviderAllowLogout() && userService.isUserValid();
    };

    userService.isProviderAllowLogout = function () {
      return userService.allowTinypassAccountsLogin()
              || userService.isCondeUserProvider()
              || userService.isPianoIdUserProvider()
              || userService.isGigyaUserProvider()
              || userService.isJanrainUserProvider();
    };

    userService.logout = function () {
      if (userService.isProviderAllowLogout()) {
        eventService.postMessage("logout", {closeOnLogout: getParameterByName("closeOnLogout")});
      }
      utilsService.removeExtendExpiredAccessCookie(userService.getConfig().app.aid);
      loginSuccessPosted = false;
    };

    userService.loginSuccessNoPostMessage = function (params) {
      return postMessageLoginSuccess(params);
    };

    userService.onUserProfileUpdated = function (params) {
      eventService.postMessage("userProfileUpdateSuccess", params);
    };

    /**
     * Register function is the same as the login function except
     * that it specifies which screen start on on.  Obviously, that is register
     */
    userService.register = function () {
      return userService.login('register');
    };

    var loginDeferred;


    userService.checkUser = function (params) {
      return tpHTTP({
        method: 'get',
        url: '/checkout/user/check',
        params: params
      });
    };

    /**
     * Login method will open up a Login/Register popup if the app is configured
     * for tinypassAccounts.  Otherwise, it will fire a loginRequired event.
     */
    userService.login = function (startScreen, customParams) {
      function _generatePopup(iframeParams, src) {
        var width = parseInt(getParameterByName("parentWidth"), 10);
        var height = parseInt(getParameterByName("parentHeight"), 10);
        var w = iframeParams.width ? iframeParams.width : width;
        var h = iframeParams.height ? iframeParams.height : height;
        // Fixes dual-screen position   Most browsers   Firefox

        var dualScreenLeft = parseInt(getParameterByName("parentDualScreenLeft"), 10);
        var dualScreenTop = parseInt(getParameterByName("parentDualScreenTop"), 10);
        var parentOuterHeight = parseInt(getParameterByName("parentOuterHeight"), 10);

        var left = ((width / 2) - (w / 2)) + dualScreenLeft;
        var top = ((parentOuterHeight / 2) - (h / 2)) + dualScreenTop;

        var newWindow = window.open(src, loginWindowId, 'scrollbars=yes,status=0,toolbar=0,resizable=1, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left);

        // Puts focus on the newWindow
        //noinspection JSUnresolvedVariable
        if (window.focus) {
          newWindow.focus();
        }

        return newWindow;
      }

      /* Default start screen is login */
      if (!startScreen) {
        startScreen = 'login';
      }

      var aid = getParameterByName("aid");
      var hostUrl = getParameterByName("url");
      loginDeferred = $q.defer();
      var promise = loginDeferred.promise;

      eventService.postMessage("loginStart", {});

      promise.finally(function () {
        eventService.postMessage("loginEnd", {});
      });

      util.log('[USER SERVICE] Cannot continue without valid user...sending loginRequiredEvent');
      if (userService.allowTinypassAccountsLogin()) {
        loginSuccessPosted = false;
        var iframeParams = {
          displayMode: "popup",
          aid: aid,
          width: 360,
          height: 660,
          state: startScreen,
          iframeId: "login-" + _randomString(10),
          host_url: hostUrl,
          url: window.location.href
        };
        userService.getConfig().fillLoginIframeParams(iframeParams);

        var src = TPParam.LOGIN + "?" + $.param(iframeParams, true) + '#/' + startScreen;

        loginWindow = _generatePopup(iframeParams, src);


        ngEventService.subscribe('readyToSubmitStats', function () {
          eventService.customPostMessage('submitStats', userService.getConfig().stats, document.location.origin, loginWindow);
        });

        ngEventService.subscribe("loginSuccess", function (event, params) {
          params = params || {};
          params.reloadAfterLogin = true;

          loginDeferred.resolve(params);
          onExternalLogin(params);
          if (loginWindow && loginWindow.closed === false) {
            try {
              loginWindow.document.body.innerHTML = null;
            } catch (e) {
            }
            loginWindow.close();
          }
        });

      } else {
        var gaEventParams = {
          aid: aid
        };
        userService.getConfig().fillLoginRequiredParamsToGoogleAnalytics(gaEventParams);

        gaService.sendGoogleAnalyticsEvent({
          eventAction: 'loginRequired', eventLabel: 'Login required',
          params: gaEventParams
        });

        var loginRequiredEventParams = {};
        loginRequiredEventParams.startScreen = startScreen;
        userService.getConfig().fillLoginRequiredEventParams(loginRequiredEventParams);

        fillCustomParams(loginRequiredEventParams, customParams);

        eventService.loginRequiredEvent(loginRequiredEventParams).then(function (object) {
          if (object.callbackNotFound || object.result !== false) {
            loginDeferred.reject(false);
            errorService().terminal(lang.trc("checkout.platform", "Cannot start checkout. User not logged in"));
          }
        });
      }
      return loginDeferred.promise;
    };

    function fillCustomParams(eventParams, customParams) {
      if (customParams) {
        return Object.assign(eventParams, customParams);
      }

      return eventParams;
    }

    userService.onLoginSuccess = function (callback, oneTime) {
      if (callback) {
        loginSuccessCallbacks.push({callback: callback, oneTime: oneTime})
      }
    };

    userService.cleanSuccessLoginCallbacks = function () {
      loginSuccessCallbacks = utilsService.filter(loginSuccessCallbacks, function (c) {
        return !c.oneTime;
      });
    };

    userService.loginSuccessPosted = function () {
      return loginSuccessPosted;
    };

    userService.setLoginSuccessPosted = function (value) {
      loginSuccessPosted = value;
    };

    userService.onExternalLogin = onExternalLogin;

    /**
     * handle event from modal Login window
     */
    ngEventService.subscribe('changeLocale', function (event, params) {
      if (params && params.locale) {
        lang.list()
                .then(function (list) {
                  for (var index in list) {
                    if (list[index].locale === params.locale) {
                      lang.update(params.locale);
                      break;
                    }
                  }
                });
      }
    });

    ngEventService.subscribe("externalLoginSuccess", function (event, params) {
      params = params || {};
      params.reloadAfterLogin = true;
      onExternalLogin(params);
    });

    function onExternalLogin(params) {
      if (!!params.extendExpiredAccessEnabled) {
        utilsService.setExtendExpiredAccessCookie(userService.getConfig().app.aid, params.extendExpiredAccessEnabled);
      }
      return userService.loginSuccessNoPostMessage(
              params
      ).then(function () {
        if (loginDeferred) {
          loginDeferred.resolve(params);
        }
      });
    }

    function postMessageLoginSuccess(params) {
      var deferred = $q.defer();
      if (!loginSuccessPosted) {
        loginSuccessPosted = true;

        if (!params) {
          params = {};
        }

        userService.getConfig().fillLoginSuccessEventParams(params);

        params.resultCallback = function () {

          angular.forEach(loginSuccessCallbacks, function (c) {
            c.callback(params);
          });

          loginSuccessCallbacks = utilsService.filter(loginSuccessCallbacks, function (c) {
            return !c.oneTime;
          });

          deferred.resolve(params);
        };

        eventService.postMessage("loginSuccess", params);
      } else {
        deferred.reject(false);
      }
      return deferred.promise;
    }

    userService.userChanged = function (user) {
      if (loginDeferred) {
        loginDeferred.resolve(user);
      }
    };

    return userService;
  }]);

var containerServiceModule = angular.module('containerServiceModule', ['ui.router', 'ngSanitize', 'generalModule']);

containerServiceModule.factory('containerService', [
  '$window', '$rootScope', '$rootElement', '$timeout', '$interval',
  '$document', 'eventService', 'errorService',
  'windowStateService', 'ngEventService'
  , function (
    $window, $rootScope,
    $rootElement, $timeout, $interval, $document, eventService, errorService,
    windowStateService, ngEventService
  ) {
    var containerService = {};
    var currentResizeSubscribers = {};
    var resizeIframeEl;
    var resizeIframeElDoc;
    containerService.$rootScope = $rootScope;
    containerService.rootElement = $rootElement;

    $(document)
      .on('focus', 'input, textarea, select', function () {
        eventService.postMessage('inputFocus');
      })
      .on('blur', 'input, textarea, select', function () {
        eventService.postMessage('inputBlur');
      });

    function getNewHeight(resizeIframeEl, resizeIframeElDoc) {
      var iframeBody = resizeIframeEl.document ? resizeIframeEl.document.body : resizeIframeEl.body;
      var iframeDocument = resizeIframeElDoc.documentElement || {};

      return Math.max(
              iframeBody.scrollHeight, iframeDocument.scrollHeight,
              iframeBody.offsetHeight, iframeDocument.offsetHeight,
              iframeBody.clientHeight, iframeDocument.clientHeight
      );
    }

    function onResizeHandler(resizeIframeEl, resizeIframeElDoc, iframeSizes) {
      var newHeight = getNewHeight(resizeIframeEl, resizeIframeElDoc);

      if (newHeight !== iframeSizes.lastHeight) {
        iframeSizes.lastHeight = newHeight;
        containerService.resize();
      }
    }

    function polyfillRemove() {
      if (!('remove' in Element.prototype)) {
        Element.prototype.remove = function() {
          if (this.parentNode) {
            this.parentNode.removeChild(this);
          }
        };
      }
    }

    containerService.getConfig = function () {
      return {
        container: this._getConfigFn().container
      };
    };

    containerService.getPreferredWidth = function () {
      return this.preferredWidth;
    };

    containerService.setPreferredWidth = function (newPreferredWidth) {
      this.preferredWidth = newPreferredWidth;
    };

    containerService.setConfigWidth = function(newConfigWidth) {
      this.widthFromConfigDirective = newConfigWidth;
    }

    containerService.init = function (options, getConfigFn) {
      this._getConfigFn = getConfigFn;

      var displayModeParam = getParameterByName('displayMode');
      this.isModal = displayModeParam === "modal";
      this.isPopup = displayModeParam === "popup";
      this.isInline = displayModeParam === "inline";
      this.isPreview = !!getParameterByName('preview');
      this.isOpenedFromInlineCheckout = getParameterByName('isOpenedFromInlineCheckout') === 'true';

      this.container = this.getConfig().container;
      this.suggestedWidth = getIntParam('width', 1);
      this.preferredWidth = options.preferredWidth;

      containerService.clearResizeHandler();
      containerService.initResizeHandler(this.container);
    };

    containerService.clearResizeHandler = function () {
      currentResizeSubscribers.interval && $interval.cancel(currentResizeSubscribers.interval);
      polyfillRemove();
      currentResizeSubscribers.handleResizeIFrame && currentResizeSubscribers.handleResizeIFrame.remove();
      currentResizeSubscribers.resizeObs && currentResizeSubscribers.resizeObs.disconnect();
      currentResizeSubscribers.loaded && currentResizeSubscribers.loaded();
      currentResizeSubscribers = {};
    };

    containerService.initResizeHandler = function (container) {
      var handleResizeFrame = document.createElement('iframe');
      currentResizeSubscribers.handleResizeIFrame = handleResizeFrame;
      var iframeSizes = { lastHeight: -1 };
      container.css('position', 'relative');

      handleResizeFrame.width = '100%';
      handleResizeFrame.height = '100%';
      handleResizeFrame.setAttribute('style', 'position: absolute !important; z-index: -1 !important;');

      container.prepend(handleResizeFrame);

      resizeIframeEl = (handleResizeFrame.contentWindow || handleResizeFrame.contentDocument);
      resizeIframeElDoc = handleResizeFrame.contentDocument || {};
      var onResizeHandlerFn = onResizeHandler.bind(this, resizeIframeEl, resizeIframeElDoc, iframeSizes);

      if (typeof ResizeObserver === 'function') {
        var ro = new ResizeObserver(onResizeHandlerFn);
        ro.observe(handleResizeFrame);
        currentResizeSubscribers.resizeObs = ro;
      } else {
        resizeIframeEl.onresize = onResizeHandlerFn;
      }

      currentResizeSubscribers.interval = $interval(onResizeHandlerFn, 500);

      window.onload = function () {
        containerService.resize();
      };

      containerService.onReady(function () {
        containerService.resize();
      });
      //rootElement.height calc right at div with display:block only in Internet Explorer
      //This resize is called after parent block set display:block
      currentResizeSubscribers.loaded = ngEventService.subscribe('loaded', function () {
        containerService.resize();
      });
    };

    containerService.onReady = function (fn) {
      // in case the document is already rendered
      if (document.readyState !== 'loading') fn();
      // modern browsers
      else if (document.addEventListener) document.addEventListener('DOMContentLoaded', fn);
      // IE <= 8
      else document.attachEvent('onreadystatechange', function () {
          if (document.readyState === 'complete') fn();
        });
    };

    ngEventService.subscribe("RESIZE_MODAL_CHECKOUT", function (event, params) {
      var iframe = params.iframe;
      var width = params.width;

      setTimeout(function () {
        containerService.suggestedWidth = width;

        if (iframe.config.width) {
          containerService.suggestedWidth = (width < iframe.config.width) ? width : iframe.config.width;
        }

        containerService.resize();
      }, 100);
    });

    containerService.resize = function _resize() {
      var newHeight;
      if (resizeIframeEl && resizeIframeElDoc) {
        newHeight = getNewHeight(resizeIframeEl, resizeIframeElDoc)
      } else {
        var containerHeight = containerService.rootElement.height();
        var parentElementHeight = containerService.rootElement[0].parentElement ?
          containerService.rootElement[0].parentElement.clientHeight :
          0;
        newHeight = containerHeight ? containerHeight : parentElementHeight;
      }

      if (!containerService.isModal && !containerService.isPopup && !containerService.isPreview) {
        eventService.resizeEvent({ height: newHeight });
        return;
      }

      var newWidth = containerService.getNewWidth();

      angular.element('body').css('min-width', newWidth);
      angular.element('body').width(newWidth);
      containerService.rootElement.width(newWidth);
      containerService.container.width(newWidth);

      eventService.resizeEvent({ width: newWidth, height: newHeight });
      setTimeout(containerService.scrollTop, 500);
    };

    containerService.scrollTop = function () {
      document.defaultView.scrollTo(0, 0);
    };

    containerService.getNewWidth = function() {
      if (containerService.isModal || containerService.isPopup) {

        if (this.widthFromConfigDirective) {
          containerService.preferredWidth = this.widthFromConfigDirective;
        }

        var newWidth = containerService.suggestedWidth;

        if (containerService.preferredWidth <= newWidth || isNaN(newWidth)) {
          newWidth = containerService.preferredWidth;
        }

        return parseInt(newWidth);
      }

      return parseInt(containerService.suggestedWidth);
    }

    containerService.getCenterScreen = function (w, h) {
      w = w || 0;
      h = h || 0;

      var width = parseInt(getParameterByName("parentWidth"), 10);
      var dualScreenLeft = parseInt(getParameterByName("parentDualScreenLeft"), 10);
      var dualScreenTop = parseInt(getParameterByName("parentDualScreenTop"), 10);
      var parentOuterHeight = parseInt(getParameterByName("parentOuterHeight"), 10);

      var left = ((width / 2) - (w / 2)) + dualScreenLeft;
      var top = ((parentOuterHeight / 2) - (h / 2)) + dualScreenTop;
      return { left: left, top: top };
    };

    containerService.getOriginUrl = function () {
      return getParameterByName("url");
    };

    containerService.purge = function (closeEventParams) {
      try {
        containerService.rootElement.remove();
      } catch (e) {
      }
      eventService.purgeEvent(closeEventParams);
    };

    containerService.close = function (closeEventParams) {
      try {
        containerService.rootElement.remove();
      } catch (e) {
      }
      eventService.closeEvent(closeEventParams);
    };

    return containerService;
  }]);

angular.module('pianoIdProviderModule', [
  'generalModule'
])
  .factory('pianoIdProvider', [
    '$sce', '$window', '$q', 'lang', 'errorService', 'eventService', 'ngEventService', 'userService', 'containerService',
    function ($sce, $window, $q, lang, errorService, eventService, ngEventService, userService, containerService) {

      var isNewUser = false;
      let stage = null;
      let preloaderHeight = null;
      var locale;
      var origin;
      var iframeOrigin;

      var widgetOptions = {};

      var me = {};
      var authPianoIdParams = {};

      var viewCtrl;

      me.init = function (_widgetOptions) {
        widgetOptions = _widgetOptions;
        me.setOrigin(widgetOptions.pianoIdUrl);

        authPianoIdParams = {
          aid: widgetOptions.aid,
          checkout: widgetOptions.checkout,
          customFormParams: widgetOptions.customFormParams,
          isPasswordlessCheckoutEnabled: widgetOptions.isPasswordlessCheckoutEnabled(),
          isPreventPasswordlessLogin: widgetOptions.isPreventPasswordlessLogin,
          isSingleStepEnabled: widgetOptions.isSingleStepEnabled(),
          affiliateIssuerId: widgetOptions.getAffiliateIssuerId(),
          userLoginEmail: widgetOptions.userLoginEmail,
          passwordlessSetPaymentEmail: widgetOptions.passwordlessSetPaymentEmail,
          isSentLoginEmail: widgetOptions.isSentLoginEmail,
          isSingleStepFormShown: function () { return viewCtrl ? widgetOptions.isSingleStepFormShown(viewCtrl) : false; },
          getUserToken: widgetOptions.getUserToken,
          getPageView : widgetOptions.getPageView
        };

      };

      me.setStage = function (value) {
        stage = value;
      };

      me.getStage = function () {
        return stage;
      };

      me.setPreloaderHeight = function (value) {
        preloaderHeight = value;
      };

      me.getPreloaderHeight = function () {
        return preloaderHeight;
      };

      me.setViewCtrl = function (_viewCtrl) {
        viewCtrl = _viewCtrl;
      };

      me.getWidgetOptions = function () {
        return widgetOptions;
      };

      me.setOrigin = function (newOrigin) {
        origin = newOrigin;
        iframeOrigin = getIframeOrigin(origin);
      };

      me.register = function () {
      };

      me.login = function () {
      };

      me.resetPassword = function () {
      };

      // me.onLoginSuccess = onLoginSuccess;

      me.isNewUser = function () {
        return isNewUser;
      };

      me.setupDoubleOptInParams = function ($element, iframeOrigin) {
        return widgetOptions.getDoubleOptInParams &&
          widgetOptions.getDoubleOptInParams().then(function (checkoutParams) {
          var iframe = $element.find('iframe')[0];

          if (!iframe) {
            return;
          }
          var params = {
            tbc: checkoutParams.tbc,
            termId: checkoutParams.params.termId,
            showOfferParams: JSON.stringify(checkoutParams.params),
            trackingId: checkoutParams.trackingId
          };

          eventService.customPostMessage(
            'setupDoubleOptInParams',
            params,
            iframeOrigin,
            iframe.contentWindow);
        });
      };

      me.authPianoIdController = function ($scope, $element) {
        $scope.id = 'piano-id-' + _randomString(5);
        var aidsForNewPianoIdVersion = [
          // add publisher aid here for new piano id version
          'G18rIJWQ2G',
          '45L2BaS6pu' // the spectator
        ];
        lang.on(function (newLocale) {
          updateLang($element, newLocale);
        });

        var customFormParams = authPianoIdParams.customFormParams;
        $scope.formName = customFormParams().formName;

        updateIframeUrl();

        $scope.error_msg = lang.trc('checkout.platform', 'You should sign in or sign up first');

        var unsubLoginSuccess = ngEventService.subscribe($scope.id + '-loginSuccess', function (event, params) {
          onLoginSuccess(authPianoIdParams.aid, params);
        });

        var unsubResize = ngEventService.subscribe($scope.id + '-resize', function (event, params) {
          resize(params, $element);
        });

        var unsubCustomEvent = ngEventService.subscribe($scope.id + '-customEvent', function (event, params) {
          eventService.customEvent('customEvent', params);
        });

        var unsubLoginResponse = ngEventService.subscribe($scope.id + '-loginResponse', function (event, params) {
          ngEventService.fire('pianoIdSingleStepLoginResponse', params);
        });

        var unsubSubmitPianoIdLoginForm = ngEventService.subscribe('submitPianoIdLoginForm', function () {
          var iframe = $element.find('iframe');
          eventService.customPostMessage('submitLoginForm', null, iframeOrigin, iframe[0].contentWindow);
        });

        var unsubUpdatePianoIdAuthIframeUrl = ngEventService.subscribe('updatePianoIdAuthIframeUrl', function () {
          updateIframeUrl();
        });

        var unsubLoaded = ngEventService.subscribe($scope.id + '-loaded', function (event, params) {
          subscribeLoaded($element);
          // authPianoIdParams.setupDoubleOptInParams(iframeOrigin);
          me.setupDoubleOptInParams($element, iframeOrigin);
        });

        var unsubPianoIdEvent = ngEventService.subscribe($scope.id + '-pianoIdEvent', function (event, params) {
          if (params.event === 'passwordlessEmailSent') {
            widgetOptions.passwordlessEmailSentHandler(params.params.loginTokenId);
          }
          subscribePianoIdEvent(params, customFormParams().trackingId, params.params.formName);
        });

        var loginRequiredEventParams = {
          preventExternalLogin: true
        };

        userService.getConfig().fillLoginRequiredEventParams(loginRequiredEventParams);
        eventService.loginRequiredEvent(loginRequiredEventParams);

        function isNewPianoIdVersion() {
          return aidsForNewPianoIdVersion.indexOf(authPianoIdParams.aid) >= 0;
        }

        function getIndexForPianoId2IfNeed() {
          return isNewPianoIdVersion() ? 'index3.html' : '';
        }

        function buildUrl(base, id) {
          base += getIndexForPianoId2IfNeed();
          var containerWidth = containerService.rootElement.width();
          var query = {
            aid: authPianoIdParams.aid,
            checkout: authPianoIdParams.checkout,
            template: authPianoIdParams.template,
            sender: id,
            origin: document.location.origin || document.location.href,
            width: containerWidth,
            lang: locale || '',
            stage: me.getStage() || authPianoIdParams.affiliateIssuerId,

            site: $window.TPParam.params.url,
            screen: getAuthScreen(),
            create_passwordless_user: authPianoIdParams.isPasswordlessCheckoutEnabled && !authPianoIdParams.isPreventPasswordlessLogin(),
            single_step: authPianoIdParams.isSingleStepFormShown(),
            display_mode: 'inline',
            tbc: authPianoIdParams.getPageView && authPianoIdParams.getPageView().tbc,
            page_view_id: authPianoIdParams.getPageView && authPianoIdParams.getPageView().pageViewId,
            tracking_id: authPianoIdParams.customFormParams().trackingId
          };

          if (me.getPreloaderHeight()) {
            query.preloader_height = me.getPreloaderHeight();
          }

          if ($scope.formName) {
            query.form_name = $scope.formName;
          } else if (!!authPianoIdParams.userLoginEmail()) {
            query.email = authPianoIdParams.userLoginEmail();
            authPianoIdParams.passwordlessSetPaymentEmail();
          }

          return [base, $.param(query)].join('?');
        }

        function getAuthScreen() {
          if (userService.isUserValid() && !userService.isUserConfirmed()) {
            return 'email_is_not_confirmed';
          }

          if (authPianoIdParams.isSentLoginEmail()) {
            return 'passwordless_confirmation';
          }

          return authPianoIdParams.isPasswordlessCheckoutEnabled ? 'login' : 'register';
        }

        function updateIframeUrl() {
          var url = buildUrl(origin, $scope.id);
          $scope.url = $sce.trustAsResourceUrl(url);
        }

        $scope.unsubscribeNgEventService = function () {
          unsubLoginSuccess();
          unsubResize();
          unsubCustomEvent();
          unsubLoginResponse();
          unsubSubmitPianoIdLoginForm();
          unsubUpdatePianoIdAuthIframeUrl();
          unsubLoaded();
          unsubPianoIdEvent();
          errorService($scope).reset();
        };

      };

      me.formPianoIdController = function ($scope, $element) {
        var submitDefer;
        var loadedDefer = $q.defer();

        var submitSuccess = false;
        $scope.isFormHidden = true;

        $scope.id = 'piano-id-' + _randomString(5);

        var customFormParams = authPianoIdParams.customFormParams;
        $scope.formName = customFormParams().formName;
        $scope.hideIfComplete = customFormParams().hideCompletedFields;

        lang.on(function (newLocale) {
          updateLang($element, newLocale);
        });
        var url = buildUrl(origin + 'form', $scope.id);

        var unsubProfileUpdated = ngEventService.subscribe($scope.id + '-profileUpdated', function (event, params) {
          submitSuccess = true;
          if (submitDefer) {
            submitDefer.resolve(true);
          }
        });

        var unsubScopeIdChanged = ngEventService.subscribe($scope.id + '-change', function (event, params) {
          submitSuccess = false;
        });

        var unsubProfileUpdatedError = ngEventService.subscribe($scope.id + '-profileUpdatedError', function (event, params) {
          submitSuccess = false;
          if (submitDefer) {
            submitDefer.resolve(false);
          }
        });

        var unsubPianoIdEvent = ngEventService.subscribe($scope.id + '-pianoIdEvent', function (event, params) {

          if (params.event === 'cfFormImpression') {
            $scope.$apply(function () {
              $scope.isFormHidden = false;
            });
          }

          subscribePianoIdEvent(params, customFormParams().trackingId, params.params.formName);
        });

        var unsubResize = ngEventService.subscribe($scope.id + '-resize', function (event, params) {
          resize(params, $element);
        });

        var unsubLoaded = ngEventService.subscribe($scope.id + '-loaded', function (event, params) {
          subscribeLoaded($element);
          loadedDefer.resolve();
        });

        var unsubStateReady = ngEventService.subscribe($scope.id + '-stateReady', function (event, params) {
          var iframe = $element.find('iframe');
          eventService.customPostMessage('setToken', authPianoIdParams.getUserToken(), iframeOrigin, iframe[0].contentWindow);
        });

        function buildUrl(base, id) {
          var containerWidth = containerService.rootElement.width();
          var query = {
            form_name: $scope.formName,
            hide_if_complete: $scope.hideIfComplete,

            aid: authPianoIdParams.aid,
            checkout: authPianoIdParams.checkout,
            sender: id,
            origin: document.location.origin || document.location.href,
            width: containerWidth,
            lang: locale || '',
            stage: me.getStage() || authPianoIdParams.affiliateIssuerId,
            access_token: authPianoIdParams.getUserToken(),

            passwordless: authPianoIdParams.isPasswordlessCheckoutEnabled,
            tbc: authPianoIdParams.getPageView || authPianoIdParams.getPageView().tbc,
            page_view_id: authPianoIdParams.getPageView || authPianoIdParams.getPageView().pageViewId
          };


          if (me.getPreloaderHeight()) {
            query.preloader_height = me.getPreloaderHeight();
          }

          return [base, $.param(query)].join('?');
        }

        $scope.url = $sce.trustAsResourceUrl(url);

        $scope._isValid = function () {
          return loadedDefer.promise
            .then(function () {
              if ($scope.isFullFilled || submitSuccess) {
                return true;
              }

              var iframe = $element.find('iframe');

              if (!submitSuccess) {
                submitDefer = $q.defer();
                eventService.customPostMessage('submit', {}, iframeOrigin, iframe[0].contentWindow);
              }

              return submitDefer.promise;
            });
        };

        $scope.unsubscribeNgEventService = function () {
          unsubProfileUpdated();
          unsubScopeIdChanged();
          unsubProfileUpdatedError();
          unsubPianoIdEvent();
          unsubResize();
          unsubLoaded();
          unsubStateReady();
        };

      };

      me.emailConfirmationRequiredController = function ($scope, $element) {
        $scope.id = 'piano-id-' + _randomString(5);
        lang.on(function (newLocale) {
          updateLang($element, newLocale);
        });

        var url = buildUrl(origin, $scope.id);
        $scope.url = $sce.trustAsResourceUrl(url);
        var customFormParams = authPianoIdParams.customFormParams;

        var unsubResize = ngEventService.subscribe($scope.id + '-resize', function (event, params) {
          resize(params, $element);
        });

        var unsubLoaded = ngEventService.subscribe($scope.id + '-loaded', function () {
          subscribeLoaded($element);
          // emailConfirmationParams.setupDoubleOptInParams(iframeOrigin);
          me.setupDoubleOptInParams($element, iframeOrigin);
        });

        var unsubPianoIdEvent = ngEventService.subscribe($scope.id + '-pianoIdEvent', function (event, params) {
          subscribePianoIdEvent(params, customFormParams().trackingId);
        });

        function buildUrl(base, id) {
          var containerWidth = containerService.rootElement.width();
          var query = {
            aid: authPianoIdParams.aid,
            sender: id,
            origin: document.location.origin || document.location.href,
            site: $window.TPParam.params.url,
            screen: 'email_confirmation_required',
            checkout: authPianoIdParams.checkout,
            width: containerWidth,
            lang: locale || '',
            passwordless: authPianoIdParams.isPasswordlessCheckoutEnabled,
            stage: me.getStage() || authPianoIdParams.affiliateIssuerId,
            access_token: authPianoIdParams.getUserToken(),

            tbc: authPianoIdParams.getPageView || authPianoIdParams.getPageView().tbc,
            page_view_id: authPianoIdParams.getPageView || authPianoIdParams.getPageView().pageViewId
          };

          if (me.getPreloaderHeight()) {
            query.preloader_height = me.getPreloaderHeight();
          }

          return [base, $.param(query)].join('?');
        }

        $scope.$on('$destroy', function () {
          unsubResize();
          unsubLoaded();
          unsubPianoIdEvent();
        });
      };

      function onLoginSuccess(aid, data) {
        var user = data.user;
        user.firstName = user.given_name;
        user.lastName = user.family_name;
        user.valid = true;
        user.confirmed = !(data.user && data.user.email_confirmation_required);
        user.uid = user.sub;

        isNewUser = !!data.registration;
        var hostUrl = containerService.getOriginUrl();

        userService.checkUser({
          aid: aid,
          url: hostUrl,
          userToken: data.token,
          fromUserIdentity: true
        }).then(function (checkUserRes) {
          return checkUserRes.models.token_list;
        }).catch(function () {
          return null;
        }).then(function (tokenList) {
          userService.onExternalLogin({
            token_list: tokenList,
            cookie_domain: data.cookie_domain,
            user_token: data.token,
            params: data.user,
            user: user,
            registration: isNewUser,
            extendExpiredAccessEnabled: data.extendExpiredAccessEnabled,
            stage: data.stage
          });
        });
      }

      function getIframeOrigin(url) {
        if (url.indexOf('http') === 0) {
          return url;
        } else {
          return document.location.origin || document.location.href;
        }
      }

      function updateLang($element, newLocale) {
        locale = newLocale || locale;
        var iframe = $element.find('iframe')[0];
        if (!iframe) {
          return;
        }
        eventService.customPostMessage('lang', { lang: locale }, iframeOrigin, iframe.contentWindow);
      }

      function resize(params, $element) {
        if (params.height === 0) {
          return;
        }

        var height = {
          height: params.height + 'px'
        };
        var elements = $element.find('#piano-id-container, iframe');

        if (params.animate) {
          elements.animate(height, 200);
        } else {
          elements.css(height);
        }

        containerService.resize();
      }

      function subscribeLoaded($element) {
        var container = $element.parent();
        var iframe = $element.find('iframe');
        var containerWidth = container.width();
        var center = containerService.getCenterScreen();
        var originUrl = containerService.getOriginUrl();

        var params = {
          width: containerWidth,
          center: center,
          originUrl: originUrl
        };
        eventService.customPostMessage('resize', params, iframeOrigin, iframe[0].contentWindow);
        updateLang($element);
      }

      function subscribePianoIdEvent(params, trackingId, formName) {
        var additionParams = {
          trackingId: trackingId,
          formName: formName
        };

        var extendedParams = {
          event: params.event,
          params: $.extend({}, params.params, additionParams),
          iframeId: getParameterByName('iframeId')
        };

        eventService.postMessage('pianoIdEvent', extendedParams);
      }

      return me;
    }])

  .directive('pianoId', ['userService', 'pianoIdProvider', '$injector',
    function (userService, pianoIdProvider, $injector) {
      return {
        restrict: 'EA',
        require: '?^view',
        template: '<div id="piano-id-container">' +
          '  <auth-piano-id ng-if="authIsExist()"></auth-piano-id>' +
          '  <form-piano-id ng-if="formIsExist()"></form-piano-id>' +
          '</div>',
        link: function ($scope, $element, $attrs, viewCtrl) {
          var viewService;
          let hasStage = !!$attrs.stage;
          let stage = null;

          try {
            viewService = $injector.get('viewService');
          } catch (e) {
          }

          $scope.formIsExist = function () {
            return userService.isUserValid()
                    && !pianoIdProvider.isNewUser()
                    && pianoIdProvider.getWidgetOptions().widgetSpecificCheckIfVisible();
          };
          $scope.authIsExist = function () {
            return (!viewCtrl || viewService.isActive(viewCtrl.stateName))
                    && !(userService.isUserValid() && userService.isUserConfirmed())
                    && (!hasStage || (hasStage && stage));
          };

          $scope.isUserValid = userService.isUserValid;
          pianoIdProvider.setPreloaderHeight($attrs.preloaderHeight);

          if (hasStage) {
            $scope.$watch($attrs.stage, function (value) {
              stage = value;
              pianoIdProvider.setStage(stage);
            });
          }
        }
      };
    }])
  .directive('emailConfirmationRequired', ['pianoIdProvider', '$injector',
    function (pianoIdProvider, $injector) {
      return {
        restrict: 'EA',
        scope: {},
        require: '^view',
        template: '<iframe id="{{id}}" ng-src="{{url}}" style="width:100%;"></iframe>',
        link: function ($scope, $element, attrs, viewCtrl) {
          var viewService;
          try {
            viewService = $injector.get('viewService');
          } catch (e) {}

          if (!viewService.isActive(viewCtrl.stateName)) {
            $element.remove();
            return;
          }

          if (viewCtrl) {
            pianoIdProvider.setViewCtrl(viewCtrl);
          }

          pianoIdProvider.emailConfirmationRequiredController($scope, $element);
        }
      };
    }])
  .directive('authPianoId', ['pianoIdProvider', 'errorService',
    function (pianoIdProvider, errorService) {
      return {
        restrict: 'EA',
        scope: {},
        require: '?^view',
        template: '<iframe id="{{id}}" ng-src="{{url}}" style="width:100%;"></iframe>',
        link: function ($scope, $element, attrs, viewCtrl) {
          var widgetOptions = pianoIdProvider.getWidgetOptions();

          if (viewCtrl) {
            pianoIdProvider.setViewCtrl(viewCtrl);

            if (viewCtrl.stateName !== 'auth') {
              viewCtrl.registerComponentController($scope, {
                passwordlessLoginForm: widgetOptions.isPasswordlessCheckoutEnabled
              });

              $scope._isValid = function () {
                errorService($scope).reset();

                if (widgetOptions.isSingleStepEnabled()) {
                  return true;
                }

                errorService($scope).global($scope.error_msg);
                return false;

              };

              $scope.$on('$destroy', function () {
                viewCtrl.unregisterComponentController($scope);
              });
            }
          }

          pianoIdProvider.authPianoIdController($scope, $element);
        }
      };
    }])
  .directive('formPianoId', ['pianoIdProvider', function (pianoIdProvider) {
    return {
      restrict: 'EA',
      require: '?^view',
      scope: {},
      template: '<iframe ng-hide="isFormHidden" id="{{id}}" ng-src="{{url}}" style="width:100%;"></iframe>',
      link: function ($scope, $element, attrs, viewCtrl) {
        if (viewCtrl) {
          pianoIdProvider.setViewCtrl(viewCtrl);

          if (viewCtrl.stateName === 'auth') {
            $element.remove();
            return;
          }
          viewCtrl.registerComponentController($scope);
        }

        $scope.$on('$destroy', function () {
          $scope.isFullFilled = true;
          if (viewCtrl) {
            viewCtrl && viewCtrl.unregisterComponentController($scope);
          }
        });

        pianoIdProvider.formPianoIdController($scope, $element);
      }
    };
  }]);

'use strict';

angular.module('creditRedemptionModule', [
  'generalModule'
])
  .factory('creditRedemptionService', ['eventService', 'userService', function (eventService, userService) {
    const me = {
      affiliateState: {}
    };

    function getAffiliateCreditState() {
      if (me.affiliateState && me.affiliateState.creditStates && me.affiliateState.creditStates.length > 0) {
        return me.affiliateState.creditStates[0];
      }

      return null;
    }

    function getRedeemedItemWithMinimalExpirationTS(redeemedItems) {
      let foundItem = null;

      if (!(redeemedItems && redeemedItems.length)) {
        return foundItem;
      }

      redeemedItems.forEach(function (item) {
        if (!foundItem || foundItem.exp > item.exp) {
          foundItem = item;
        }
      });

      return foundItem;
    }

    me.setAffiliateState = function (affiliateState) {
      me.affiliateState = affiliateState;
    };

    me.credits = function () {
      const creditState = getAffiliateCreditState();

      return creditState ? creditState.total - creditState.spent : 0;
    };

    me.creditsTotal = function () {
      const creditState = getAffiliateCreditState();

      return creditState ? creditState.total : 0;
    };

    me.creditExpirationTime = function () {
      const creditState = getAffiliateCreditState();
      let redeemedItem = null;
      let expirationTime = new Date();

      if (creditState) {
        redeemedItem = getRedeemedItemWithMinimalExpirationTS(creditState.redeemedItems);
      }

      if (redeemedItem && redeemedItem.exp) {
        expirationTime = new Date(redeemedItem.exp * 1000);
      }

      return expirationTime;
    };

    me.redeemCredit = function () {
      const creditState = getAffiliateCreditState();

      if (creditState) {
        eventService.postMessage('redeemCredit', {
          cid: creditState.cid,
          itemId: me.affiliateState.redemptionCandidateItemId,
          newRedemptionMethod: me.redemptionMethod()
        });
      }
    };

    me.toggleRedemptionMethod = function () {
      const creditState = getAffiliateCreditState();

      if (creditState) {
        creditState.redemptionMethod = creditState.redemptionMethod === 'A' ? 'M' : 'A'
      }
    };

    me.redemptionMethod = function () {
      const creditState = getAffiliateCreditState();

      if (creditState) {
        return creditState.redemptionMethod;
      }

      return null;
    };


    me.setupScope = function ($scope) {
      $scope.affiliateState = me.affiliateState;
      $scope.credits = me.credits;
      $scope.creditsTotal = me.creditsTotal;
      $scope.creditExpirationTime = me.creditExpirationTime;
      $scope.redeemCredit = me.redeemCredit;
      $scope.toggleRedemptionMethod = me.toggleRedemptionMethod;
      $scope.redemptionMethod = me.redemptionMethod;
    };

    return me;
  }])
;

var tpDrop = function ($injector, $document, $rootScope, $q, $http, $templateCache, $controller, $compile, $interval, $timeout) {
    var DROPDOWN_CONTAINER_SELECTOR = '.tp-dropdown__container';
    var SEARCH_FIELD_SELECTOR = '.search-query';
    var DROPDOWN_LIST = '.tp-dropdown__list';
    var DROPDOWN_ITEM_LINK = '.tp-dropdown__link';
    var DROPDOWN_SELECT = '.tp-dropdown__select';
    var ENTER_KEY = 'Enter';
    var ESCAPE_KEY = 'Escape';
    var DOWN_ARROW_KEY = 'ArrowDown';
    var UP_ARROW_KEY = 'ArrowUp';
    var INPUT_TAG_NAME = 'INPUT';

  return function (opts) {
    return $q
      .all({
        tpl: getTemplate(opts),
        opts: $q.when(opts)
      })
      .then(show)
  };

  function calculateCenteredCoordinates(root, opts) {
    var isHorizontalAxis = opts.side === 'right' || opts.side === 'left';
    var prop = isHorizontalAxis ? 'top' : 'left';
    var char = isHorizontalAxis ? 'height' : 'width';
    var oChar = isHorizontalAxis ? 'outerHeight' : 'outerWidth';

    var res = prop === 'left' ?
      makePositionFromStart(prop, char, oChar, root, opts) :
      makeCentering(prop, char, oChar, root, opts);

    if (opts.side === 'right') {
      res = makePostShifting('left', 'outerWidth', opts, res);
    }
    else if (opts.side === 'left') {
      res = makePreShifting('right', 'width', 'left', 'scrollLeft', root, opts, res);
    }
    else if (opts.side === 'top') {
      res = makePreShifting('bottom', 'height', 'top', 'scrollTop', root, opts, res);
    }
    else /* (opts.side === 'bottom'*/{
      res = makePostShifting('top', 'outerHeight', opts, res);
    }

    return res;

    function makePreShifting(prop, char, otherProp, scroll, root, opts, res) {
      res[prop] = root[char]() - calcOffset(opts.centerOn, otherProp) - root[scroll]();
      return res;
    }

    function makePostShifting(prop, char, opts, res) {
      res[prop] = calcOffset(opts.centerOn, prop) + opts.centerOn[char]();
      return res;
    }

    function makePositionFromStart(prop, char, oChar, root, opts) {
      var res = {};
      res[prop] = !isRTL(root) ?
        calcOffset(opts.centerOn, prop) :
        calcRtlOffset(prop, char, oChar, root, opts);
      return res;
    }

    function makeCentering(prop, char, oChar, root, opts) {
      var res = {};
      res[prop] = !isRTL(root) ?
        calcOffset(opts.centerOn, prop) + opts.centerOn[oChar]() / 2 - opts.style[char] / 2
        : calcOffset(opts.centerOn, prop) + opts.centerOn[oChar]() / 2 - opts.style[char] / 2 + (opts.centerOn[oChar]() - opts.style[char]);
      return res;
    }

    function calcRtlOffset(prop, char, oChar, root, opts) {
      var selectorWidth = opts.centerOn[oChar]();
      var dropdownWidth = opts.style[char];

      return calcOffset(opts.centerOn, prop) + (selectorWidth - dropdownWidth);
    }

    function calcOffset(el, prop) {
      return el.offset()[prop];
    }
  }

  function isRTL(root) {
    return root.context.dir === 'rtl';
  }

  function calcCenter(root, opts) {
    return {
      left: root.width() / 2 - opts.style.width / 2,
      top: root.height() / 2 - opts.style.height / 2
    }
  }

  function calculate(root, opts) {
    return opts.centerOn
      ? calculateCenteredCoordinates(root, opts)
      : calcCenter(root, opts);
  }

  function findRoot(opts) {
    if (opts.centerOn) {
      var fixedParent = opts
        .centerOn
        .parents()
        .filter(function () {
          var p = $(this).css('position');
          return (p === 'fixed' || p === 'absolute' ) && $(this).hasClass('modal') === false;
        })
        .last();
      return fixedParent.length > 0 ? fixedParent : $document.find('body');
    }
    else {
      return $document.find('body');
    }
  }

  function show(input) {
    var state = $q.defer(),
      tpl = input.tpl,
      opts = input.opts || {style: {width: 200}},
      root = findRoot(opts),
      scope = (opts.scope || $rootScope).$new(),
      $documentBody = $document.find('body'),
      coordinates,
      sideClass;

    scope.$close = close;
    scope.$dismiss = dismiss;

    var rejecter = opts.rejecter || byOuterClick();
    rejecter.then(dismiss, dismiss);

    if (opts.controller) {
      $controller(opts.controller, {$scope: scope})
    }

    var el = $compile(angular.element(tpl))(scope);
    root.append(el);
    $documentBody.addClass('component-opened');

    var positionElement = function () {
      reverseOverflowingSide(opts, el);
      sideClass = opts.side || 'bottom';
      coordinates = calculate(root, opts);
      decorateElement();
      setFocusToSearchInput();
      addKeydownListener();
    };
    if (scope.type && scope.type === 'enchained') {
      var intervalId = $interval(positionElement, 50);
      var cancelInterval = function () {
        $interval.cancel(intervalId)
      };
      state.promise.then(cancelInterval, cancelInterval);
    } else {
      $timeout(positionElement, 0);
    }

    return state.promise;

    function setFocusToSearchInput() {
        var $dropdownContainer = angular.element(DROPDOWN_CONTAINER_SELECTOR);
        if ($dropdownContainer.is(':visible')) {
            $dropdownContainer.find(SEARCH_FIELD_SELECTOR).focus();
        }
    }

    function byOuterClick() {
      var defer = $q.defer();
      $document.on('click', dismissByClick);
      return defer.promise;

      function dismissByClick(event) {
        $document.off('click', dismissByClick);
        if (event) {
          event.preventDefault();
          event.stopPropagation();
        }
        defer.reject();
      }
    }

    function close(res) {
      el.remove();
      state.resolve(res);
      $documentBody.removeClass('component-opened');
      angular.element(DROPDOWN_SELECT).focus();
    }

    function dismiss(res) {
      el.remove();
      state.reject(res);
      $documentBody.removeClass('component-opened');
    }

    function decorateElement() {
      el.css({//todo remove, it should be realized as child provider's
        display: 'block',
        position: 'absolute'
      })
        .on('click', function (event) {
          event.preventDefault();
          event.stopPropagation();

          if (opts.closeOnSelect) {
            dismiss();
          }
        })
        .addClass(sideClass)
        .addClass(opts.cl)
        .css(coordinates)
        .css(opts.style);
    }

    function reverseOverflowingSide(opts, el) {
      switch (opts.side) {
        case 'right':
          if ((opts.centerOn.offset().left + opts.centerOn.width() + el.width()) > window.innerWidth) {
            opts.side = 'left';
          }
          break;
        case 'left':
          if ((opts.centerOn.offset().left - el.width()) < 0) {
            opts.side = 'right';
          }
          break;
        case 'bottom':
          if ((opts.centerOn.offset().top + opts.centerOn.height() + el.height()) > window.innerHeight) {
            opts.side = 'top';
          }
          break;
        case 'top':
          if ((opts.centerOn.offset().top - el.height()) < 0) {
            opts.side = 'bottom';
          }
          break;
      }
    }

    function addKeydownListener() {
      var $dropdownList = angular.element(DROPDOWN_LIST);
      $dropdownList.on('keydown', function(e) {
        handleKeydown(e);
      });
    }

    function handleKeydown(e) {
      switch (e.key) {
        case ESCAPE_KEY:
          close();
          break;
        case ENTER_KEY:
          e.target.click();
          break;
        case DOWN_ARROW_KEY:
          e.preventDefault();
          if (e.target.nextElementSibling) {
            e.target.nextElementSibling.focus();
          } else {
            if (e.target.tagName === INPUT_TAG_NAME) {
              angular.element(DROPDOWN_ITEM_LINK).first().focus();
            }
          }
          break;
        case UP_ARROW_KEY:
          e.preventDefault();
          if (e.target.previousElementSibling) {
            e.target.previousElementSibling.focus();
          } else {
            if (e.target.tagName !== INPUT_TAG_NAME) {
              angular.element(SEARCH_FIELD_SELECTOR).focus();
            }
          }
          break;
      }
    }
  }

  function getTemplate(options) {
    return options.template ? $q.when(options.template) :
      $http.get(options.templateUrl, {cache: $templateCache}).then(function (result) {
        return result.data;
      });
  }
};

tpDrop.$inject = ['$injector', '$document', '$rootScope', '$q', '$http', '$templateCache', '$controller', '$compile', '$interval', '$timeout'];

angular.module('tpDrop.module', ['ngSanitize', 'generalModule'])
  .factory('tpDrop', tpDrop);

var requiredModules = [
  'exceptionHandler',
  'tpComponentsModule',
  'generalModule',
  'eventModule',
  'errorModule',
  'userServiceModule',
  'containerServiceModule',
  'tp.i18n',
  'html5.placeholder',
  'pianoIdProviderModule',
  'creditRedemptionModule'
];

switch (window.TPTemplateType) {
  case 'NEWSCYCLE_OFFER':
    requiredModules.push('newscycleModule');
    break;
  case 'ZUORA_OFFER':
    requiredModules.push('zuoraModule');
    break;
  case 'NEWSLETTER_SIGNUP':
    requiredModules.push('newsletterModule');
    break;
  case 'LICENSING_LANDING_PAGE':
  case 'LICENSING_CONTRACT_LIST':
  case 'LICENSING_REDEEM_RESULT':
    requiredModules.push('tpDrop.module');
    requiredModules.push('licensingModule');
    break;
  default:
    break;
}

var showTemplateModule = angular.module('showTemplateModule', requiredModules);

showTemplateModule.config(['$compileProvider', '$qProvider', function ($compileProvider, $qProvider) {
  if ($compileProvider.debugInfoEnabled) {
    $compileProvider.debugInfoEnabled(false);
  }

  if ($compileProvider.commentDirectivesEnabled) {
    $compileProvider.commentDirectivesEnabled(false);
  }

  if ($compileProvider.cssClassDirectivesEnabled) {
    $compileProvider.cssClassDirectivesEnabled(false);
  }

  if ($compileProvider.aHrefSanitizationWhitelist) {
    $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|data|local|ftp|mailto|file|javascript|sms):/);
  }

  if ($qProvider.errorOnUnhandledRejections) {
    $qProvider.errorOnUnhandledRejections(false);
  }
}]);

showTemplateModule.run(['interceptAjax', function (interceptAjax) {
  interceptAjax.appendLoaderTo('showTemplate');
}]);

/**
 * Main template directive
 */
showTemplateModule.directive('template', [
  'template', 'exposeTemplateParams', 'exposeCustomVariables', 'exposeActiveMeters', 'exposeCustomCookies', 'setupTrackingId', 'utilsService',
  'browserIdService',
  'errorService',
  'eventService',
  'creditRedemptionService',
  'statsService',
  function (template, exposeTemplateParams, exposeCustomVariables, exposeActiveMeters, exposeCustomCookies, setupTrackingId,
            utilsService,  browserIdService, errorService, eventService, creditRedemptionService, statsService) {
    return {
      restrict: 'A',
      link: function (scope, element) {
        element.addClass('showTemplate');
      },
      controller: ['$scope', '$element', '$attrs', 'ngEventService', 'lang', '$timeout', 'eventService', function ($scope, $element, $attrs, ngEventService, lang, $timeout, eventService) {
        var isTemplateCacheable = false;
        var options = {
          preferredWidth: $attrs.width,
          preferredHeight: $attrs.height
        };
        ngEventService.subscribe('trackStat', statsService.handleTrackStatEvent);

        template.init(options);
        piano.reloadTemplateWithUserToken = function (userToken) {
          template.reloadWithUserToken(userToken);
        };

        var unsubLangChanged = ngEventService.subscribe('langChanged', function (event, params) {
            lang.update(params.lang);
        });

        Helper.wrapMethod('isUserValid', template, $scope);
        Helper.wrapMethod('allowTinypassAccountsLogin', template, $scope);
        Helper.wrapMethod('allowTinypassAccountsLogout', template, $scope);
        Helper.wrapMethod('register', template, $scope);
        Helper.wrapMethod('login', template, $scope);
        Helper.wrapMethod('logout', template, $scope);
        Helper.wrapMethod('close', template, $scope);
        Helper.wrapMethod('closeAndRefresh', template, $scope);

        creditRedemptionService.setupScope($scope);

        $scope.credit = function () {
          return $scope.credits() === 1 ? 'credit' : 'credits';
        };

        function updateZuoraParams(zuoraSelectedRatePlanIds) {
          angular.forEach(zuoraSelectedRatePlanIds, function (item) {
            var selectedRatePlan = null;

            if (item.type == 'product') {
              var selectedProduct = utilsService.find(TPParam.zuoraInternal.products, function (product) {
                return product.id === item.sourceId;
              });
              if (selectedProduct && selectedProduct.productRatePlans && selectedProduct.productRatePlans.length > 0) {
                selectedRatePlan = utilsService.find(selectedProduct.productRatePlans, function (plan) {
                  return plan.id === item.ratePlanId;
                });

                if (selectedRatePlan) {
                  $scope.zuora.selectedItems.push({
                    type: item.type,
                    product: selectedProduct,
                    ratePlan: selectedRatePlan
                  });
                }
              }

            } else {
              var selectedPromoCode = utilsService.find(TPParam.zuoraInternal.promoCodes, function (promoCode) {
                return String(promoCode.id) === item.sourceId;
              });

              if (selectedPromoCode && selectedPromoCode.ratePlanGroups && selectedPromoCode.ratePlanGroups.length > 0) {
                angular.forEach(selectedPromoCode.ratePlanGroups, function (group) {
                  if (!selectedRatePlan && group && group.length > 0) {
                    selectedRatePlan = utilsService.find(group, function (plan) {
                      return plan.id === item.ratePlanId;
                    });
                  }
                });

                var productByRatePlan = utilsService.find(TPParam.zuoraInternal.products, function (product) {
                  if (product && product.productRatePlans && product.productRatePlans.length > 0) {
                    return utilsService.find(product.productRatePlans, function (plan) {
                      return plan.id === item.ratePlanId;
                    });
                  }

                  return false;
                });

                if (selectedRatePlan) {
                  $scope.zuora.selectedItems.push({
                    type: item.type,
                    promotionCode: selectedPromoCode,
                    product: productByRatePlan,
                    ratePlan: selectedRatePlan
                  });
                }
              }
            }
          });
        }

        function updateScopeValues() {
          $scope.app = TPParam.app;
          $scope.user = TPParam.user;
          $scope.terminalError = TPParam.error;
          $scope.zuora = TPParam.zuora || {};
          $scope.zuora.selectedItems = [];
          $scope.initiated = TPParam.initiated;

          var zuoraSelectedRatePlanIds = TPParam.zuoraSelectedRatePlanIds || [];
          updateZuoraParams(zuoraSelectedRatePlanIds);

          $scope.zuora.getProductBySku = function (productCode) {
            return utilsService.find(TPParam.zuoraInternal.products, function (product) {
              return product.sku === productCode;
            }) || {};
          };

          var params = {};
          angular.extend(params, TPParam.params);
          $scope.params = exposeTemplateParams(params);
          $scope.custom = exposeCustomVariables(params.customVariables || {});
          $scope.customCookies = exposeCustomCookies(params.customCookies || {});
          $scope.activeMeters = exposeActiveMeters(params.activeMeters || []);

          browserIdService.getBrowserId().then(function (browserId) {
            $scope.browserId = browserId;
          });

          if ($scope.params.trackingId) {
            setupTrackingId($scope.params.trackingId);
          }

          $scope.isShowBoilerplateCloseButton = template.isShowBoilerplateCloseButton.bind(this, isTemplateCacheable);
        }

        function applyTemplateContext(userDataContext) {
          $timeout(function() {
            var userContext = userDataContext.userContext;
            isTemplateCacheable = true;
            angular.extend(TPParam.params, userDataContext.iframeParams);
            TPParam.user = _exposeUser(userContext.user_info.user);
            TPParam.params.experienceId = userContext.experience_id;
            TPParam.params.experienceExecutionId = userContext.experience_execution_id;
            TPParam.params.experienceActionId = userContext.experience_action_id;
            if (userContext.template_language) {
              lang.update(userContext.template_language);
            }
            TPParam.GEO_COUNTRY_CODE = userContext.country_code;
            TPParam.GEO_COUNTRY_POSTAL_CODE = userContext.postal_code;
            TPParam.initiated = true;
            template.config.url = TPParam.params.url;
            template.setUser(TPParam.params.userToken, TPParam.params.userProvider);
            updateScopeValues();
            template.init(options);
            ngEventService.fire('templateInitiated');
            ngEventService.fire('updatePianoIdAuthIframeUrl');
          });
        }

        function _exposeUser(userJson) {
          var user = {};
          try {
            user = JSON.parse(userJson);
          } catch (e) {
          }
          return user;
        }

        var unsubTemplateReloaded = ngEventService.subscribe(EVENT_TEMPLATE_RELOADED, function () {
          updateScopeValues();
        });

        var unsubStatsTracked = ngEventService.subscribe(EVENT_STATS_TRACKED, function () {
          updateScopeValues();
        });

        var unsubTemplateContextDeferredLoadingInit = ngEventService.subscribe('userDataContextLoaded', function (event, params) {
          applyTemplateContext(params);
        });

        eventService.initContext().then(function (ctx) {
          $timeout(function(){
            window.TPParam.params.affiliateState = ctx.affiliateState;
            $scope.affiliateState = ctx.affiliateState;
            creditRedemptionService.setAffiliateState(ctx.affiliateState);
          }, 0);
        });

        var unsubSetupCustomVariables = ngEventService.subscribe(EVENT_SETUP_CUSTOM_VARIABLES, function (event, customVariables) {
          window.TPParam.params.customVariables = customVariables;
          $scope.custom = exposeCustomVariables(customVariables || {});
          $scope.$apply();
        });

        $scope.$on('$destroy', function () {
          unsubLangChanged();
          unsubTemplateReloaded();
          unsubStatsTracked();
          unsubTemplateContextDeferredLoadingInit();
          unsubSetupCustomVariables();
        });

        updateScopeValues();
      }]
    };
  }]);

showTemplateModule.factory('template', [
  '$window', '$rootScope', '$rootElement', '$timeout',
  'eventService', 'ngEventService', 'errorService',
  'containerService', 'userService', 'configService', 'trackExternalEvent', 'lang',
   'setupTrackingId', 'pianoIdProvider', 'updateDirAttrOnLangChanges',
  function ($window, $rootScope, $rootElement, $timeout,
            eventService, ngEventService, errorService,
            containerService, userService, configService, trackExternalEvent, lang,
             setupTrackingId, pianoIdProvider, updateDirAttrOnLangChanges
  ) {
    var template = {};
    template.$rootScope = $rootScope;
    template.rootElement = $rootElement;

    template.config = {
      "aid": TPParam.params.aid,
      "url": TPParam.params.url,
      "userToken": TPParam.params.userToken,
      "userProvider": TPParam.params.userProvider,
      "authStartScreen": TPParam.params.authStartScreen,
      pianoIdUrl: (TPParam.params.pianoIdUrl || 'https://id.tinypass.com/id/')
    };

    var anonUser = {
      displayName: null,
      email: null,
      firstName: null,
      lastName: null,
      uid: "anon",
      valid: false
    };

    var closeButtonTypes = {
      default: 'default',
      boilerplate: 'boilerplate',
    };

    template.init = function (options) {
      var templateContainer = angular.element("#template-container");

      containerService.init({preferredWidth: options.preferredWidth || 735}, function () {
        return {
          container: templateContainer
        };
      });

      if (TPParam.error) {
        errorService().terminal(lang.trc("checkout.platform", "Cannot initialize template"));
        return;
      }

      if (TPParam.consoleError) {
        util.log("%c[SHOW_TEMPLATE] Error: ".concat(TPParam.consoleError), "color: red;");
      }

      userService.init(function () {
        return {
          app: TPParam.app,
          user: TPParam.user,
          fillLoginIframeParams: function (iframeParams) {
            iframeParams.templateId = TPParam.params.templateId;
            iframeParams.templateVariantId = TPParam.params.templateVariantId;
          },
          fillLoginRequiredParamsToGoogleAnalytics: function (gaEventParams) {
            gaEventParams.templateId = TPParam.params.templateId;
            gaEventParams.templateVariantId = TPParam.params.templateVariantId;
          },
          fillLoginRequiredEventParams: function (loginRequiredEventParams) {
            loginRequiredEventParams.templateId = TPParam.params.templateId;
            loginRequiredEventParams.templateVariantId = TPParam.params.templateVariantId;
            if (template.isSiteLicensing) {
              loginRequiredEventParams.isSiteLicensing = template.isSiteLicensing;
            }
          },
          fillLoginSuccessEventParams: function (loginSuccessEventParams) {
            // nothing to do
          }
        };
      });


      // setup piano-id
      var templateWidgetOptionsForPianoIdIntegration = {
        aid: template.getConfig().aid,
        pianoIdUrl: template.getConfig().pianoIdUrl,
        checkout: 0,
        getDoubleOptInParams: undefined,
        customFormParams: template.getCustomFormParams,
        passwordlessEmailSentHandler: function () {},
        isPasswordlessCheckoutEnabled: function () {},
        isPreventPasswordlessLogin: function () {},
        isSingleStepEnabled: function () {},
        getAffiliateIssuerId: function () {}, // todo to implement this!!!!
        userLoginEmail: function () {
          return undefined;
        }, // todo to implement this!!!!
        passwordlessSetPaymentEmail: function () {},
        isSentLoginEmail: function () {
          return false;
        }, // todo maybe to implement this!!!!
        isSingleStepFormShown: function () { return false; },
        getUserToken: function () {
          return template.getConfig().userToken;
        },

        widgetSpecificCheckIfVisible: function () {
          return !!template.getCustomFormParams().formName;
        },
        getPageView : function(){
          return {
            tbc : $window.TPParam.params.tbc,
            pageViewId : $window.TPParam.params.pageViewId
          };
        }
      };
      pianoIdProvider.init(templateWidgetOptionsForPianoIdIntegration);

    };

    // TODO to add the parameters window.TPParam.params to the page
    template.getCustomFormParams = function () {
      var customFormParams = {};

      var customForm;

      var formNameByTermIdJson = window.TPParam.params.formNameByTermId;
      if (formNameByTermIdJson) {
        var formNameByTermId = JSON.parse(formNameByTermIdJson);

        var customFormTermId = co.input.customFormTermId;
        if (customFormTermId) {
          customForm = formNameByTermId[customFormTermId];
        }
      }

      if (customForm) {
        customFormParams.formName = customForm;
        customFormParams.hideCompletedFields = window.TPParam.params.hideCompletedFields || false;
      }

      customFormParams.trackingId = window.TPParam.params.trackingId;

      return customFormParams;
    };

    template.isUserValid = function () {
      return userService.isUserValid();
    };

    template.allowTinypassAccountsLogin = function () {
      return userService.allowTinypassAccountsLogin();
    };

    template.allowTinypassAccountsLogout = function () {
      return userService.allowTinypassAccountsLogout();
    };

    template.register = function () {
      if (eventService.isMobileDevice()) {
        eventService.registerEvent({});
      } else {
        userService.register();
      }
    };

    template.login = function (startScreen, customParams) {
      if (eventService.isMobileDevice()) {
        eventService.loginEvent({});
      } else {
        userService.login(startScreen, customParams);
      }
    };

    template.logout = function () {
      userService.logout();
      template.config.userToken = '';
      TPParam.user = anonUser;
      ngEventService.fire(EVENT_TEMPLATE_RELOADED);
    };

    template.close = function () {
      // close event will be logged in "checkProccessesForClose" handler

      containerService.close();
    };

    template.closeAndRefresh = function (url) {
      var externalEventParams = {};
      if (url) {
        externalEventParams['url'] = url;
      }

      trackExternalEvent('EXTERNAL_EVENT', 'close', externalEventParams);

      eventService.closeAndRefreshEvent(url);
    };

    template.reloadWithUserToken = function (userToken) {
      template.config.userToken = userToken;
      template.reload();
    };

    template.reload = function (isLoginSuccess) {
      var params = getUserCheckParams(template.config);
      params['fromUserIdentity'] = true;

      userService.checkUser(params).then(function (resp) {
        TPParam.user = resp.models.user;
        ngEventService.fire(EVENT_TEMPLATE_RELOADED);
        ngEventService.fire(EVENT_TEMPLATE_LOGIN_CHECK_FINISHED);
        if (isLoginSuccess) {
          // should be removed after TPWEBAPP-28431
          ngEventService.fire(EVENT_TEMPLATE_LOGIN_SUCCESS);
        }
      }, function () {
        errorService().terminal(lang.trc("checkout.platform", "Check user failed"));
        return false;
      });
    };

    template.isShowBoilerplateCloseButton = function(isTemplateCacheable) {
      var closeButtonType = configService.get().closeButtonType;
      var showCloseButton = isTemplateCacheable
        ? TPParam.params.showCloseButton
        : getParameterByName('showCloseButton') !== 'false';
      var isBoilerplateBtnType = closeButtonType === closeButtonTypes.boilerplate;

      return showCloseButton && isBoilerplateBtnType;
    };

    template.getConfig = function () {
      return template.config;
    };

    template.setUser = function(userToken, userProvider) {
      template.config.userToken = userToken;
      template.config.userProvider = userProvider;
    };

    ngEventService.subscribe("checkProccessesForClose", function () {
      trackExternalEvent('EXTERNAL_EVENT', 'close', {});

      containerService.purge();
    });

    ngEventService.subscribe("reloadOffer", function (e, config) {
      template.config.userToken = config.userToken;
      template.reload();
    });

    ngEventService.subscribe('successfullyCreditRedeem', function (e, params) {
      util.log('[SHOW_TEMPLATE] redeem credit successfully: ', params);
    });

    ngEventService.subscribe('creditRedeemFailed', function (e, p) {
      let message = p && p.message;
      errorService().terminal(lang.trc('checkout.platform', 'Cannot redeem credit: ') + message);
      util.log('%c[SHOW_TEMPLATE] Cannot redeem credit: '.concat(message), "color: red;");
    });

    userService.onLoginSuccess(function (params) {
      template.config.userToken = params.user_token;
      template.reload(true);
    }, false);

    updateDirAttrOnLangChanges();

    return template;

    function getUserCheckParams(config) {
      var params = {};
      var paramNames = ["aid", "url", "userToken", "userProvider", "userRef"];

      angular.forEach(paramNames, function (p) {
        params[p] = typeof config[p] !== 'undefined' ? config[p] : getParameterByName(p);
      });

      return params;
    }
  }]);

angular.element(document).ready(function () {
  window["TPInjector"] = angular.bootstrap(document.getElementById("ng-app"), ['showTemplateModule'], {
    strictDi: true
  });
});

'use strict';
angular.module("ngLocale", [], ["$provide", function($provide) {
var PLURAL_CATEGORY = {ZERO: "zero", ONE: "one", TWO: "two", FEW: "few", MANY: "many", OTHER: "other"};
$provide.value("$locale", {
  "DATETIME_FORMATS": {
    "AMPMS": [
      "AM",
      "PM"
    ],
    "DAY": [
      "Sunday",
      "Monday",
      "Tuesday",
      "Wednesday",
      "Thursday",
      "Friday",
      "Saturday"
    ],
    "MONTH": [
      "January",
      "February",
      "March",
      "April",
      "May",
      "June",
      "July",
      "August",
      "September",
      "October",
      "November",
      "December"
    ],
    "SHORTDAY": [
      "Sun",
      "Mon",
      "Tue",
      "Wed",
      "Thu",
      "Fri",
      "Sat"
    ],
    "SHORTMONTH": [
      "Jan",
      "Feb",
      "Mar",
      "Apr",
      "May",
      "Jun",
      "Jul",
      "Aug",
      "Sep",
      "Oct",
      "Nov",
      "Dec"
    ],
    "fullDate": "EEEE, MMMM d, y",
    "longDate": "MMMM d, y",
    "medium": "MMM d, y h:mm:ss a",
    "mediumDate": "MMM d, y",
    "mediumTime": "h:mm:ss a",
    "short": "M/d/yy h:mm a",
    "shortDate": "M/d/yy",
    "shortTime": "h:mm a"
  },
  "NUMBER_FORMATS": {
    "CURRENCY_SYM": "$",
    "DECIMAL_SEP": ".",
    "GROUP_SEP": ",",
    "PATTERNS": [
      {
        "gSize": 3,
        "lgSize": 3,
        "macFrac": 0,
        "maxFrac": 3,
        "minFrac": 0,
        "minInt": 1,
        "negPre": "-",
        "negSuf": "",
        "posPre": "",
        "posSuf": ""
      },
      {
        "gSize": 3,
        "lgSize": 3,
        "macFrac": 0,
        "maxFrac": 2,
        "minFrac": 2,
        "minInt": 1,
        "negPre": "(\u00a4",
        "negSuf": ")",
        "posPre": "\u00a4",
        "posSuf": ""
      }
    ]
  },
  "id": "en-us",
  "pluralCat": function (n) {  if (n == 1) {   return PLURAL_CATEGORY.ONE;  }  return PLURAL_CATEGORY.OTHER;}
});
}]);
