Source: /home/mathieu/git/scoreflex-javascript-sdk/scoreflex.js

/*
* Licensed to Scoreflex (www.scoreflex.com) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Scoreflex licenses this
* file to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

/*
 * To regenerate the documentation:
 * jsdoc -d=jsdoc/ scoreflexSDK.js
 */

/*
 * Broswer requirements / module dependancies
 * - browser support for JSON API (or json2.js at http://www.JSON.org/json2.js)
 * - browser support for localStorage API
 * - browser support for XMLHttpRequest2 (support for CORS requests)
 * - browser support for classList API
 */

/**
 * Construct a ScoreflexSDK instance.
 * @param {string} clientId
 * @param {string} clientSecret
 * @param {boolean} useSandbox
 * @return {object} Scoreflex SDK
 *
 * @module {Scoreflex} Scoreflex
 * @public
 **/
var Scoreflex = function(clientId, clientSecret, useSandbox) {
"use strict";

  var SFX = {};

  /**
   * Helper methods. Private to Scoreflex namespace.
   * @private
   * @namespace Scoreflex.Helper
   * @memberof module:Scoreflex
   */
  SFX.Helper = {
    /**
     * Test an element belongs to an Array.
     * @private
     * @param {mixed} el
     * @param {Array} arr
     * @return boolean
     */
    inArray: function(el, arr) {
      for (var i=0; i<arr.length; i++) {
        if (arr[i] === el) return true;
      }
      return false;
    },

    /**
     * Encode url parts.
     * @private
     * @param {string} str
     * @return {string} encoded string
     */
    rawurlencode: function(str) {
      // From: http://phpjs.org/functions
      // +   original by: Brett Zamir (http://brett-zamir.me)
      // +      input by: travc
      // +      input by: Brett Zamir (http://brett-zamir.me)
      // +   bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
      // +      input by: Michael Grier
      // +   bugfixed by: Brett Zamir (http://brett-zamir.me)
      // +      input by: Ratheous
      // +      reimplemented by: Brett Zamir (http://brett-zamir.me)
      // +   bugfixed by: Joris
      // +      reimplemented by: Brett Zamir (http://brett-zamir.me)
      // %          note 1: This reflects PHP 5.3/6.0+ behavior
      // %        note 2: Please be aware that this function expects to encode into UTF-8 encoded strings, as found on
      // %        note 2: pages served as UTF-8
      // *     example 1: rawurlencode('Kevin van Zonneveld!');
      // *     returns 1: 'Kevin%20van%20Zonneveld%21'
      // *     example 2: rawurlencode('http://kevin.vanzonneveld.net/');
      // *     returns 2: 'http%3A%2F%2Fkevin.vanzonneveld.net%2F'
      // *     example 3: rawurlencode('http://www.google.nl/search?q=php.js&ie=utf-8&oe=utf-8&aq=t&rls=com.ubuntu:en-US:unofficial&client=firefox-a');
      // *     returns 3: 'http%3A%2F%2Fwww.google.nl%2Fsearch%3Fq%3Dphp.js%26ie%3Dutf-8%26oe%3Dutf-8%26aq%3Dt%26rls%3Dcom.ubuntu%3Aen-US%3Aunofficial%26client%3Dfirefox-a'
      str = (str + '').toString();

      // Tilde should be allowed unescaped in future versions of PHP (as reflected below), but if you want to reflect current
      // PHP behavior, you would need to add ".replace(/~/g, '%7E');" to the following.
      return encodeURIComponent(str).replace(/!/g, '%21').replace(/'/g, '%27').replace(/\(/g, '%28').
      replace(/\)/g, '%29').replace(/\*/g, '%2A');
    },

    /**
     * Generate an universal unique identifier.
     * @private
     * @return {string} uuid
     */
    getUUID: function() {
      // generates an unique id
      // inspired by
      // https://github.com/broofa/node-uuid/blob/master/uuid.js (MIT license)
      // http://stackoverflow.com/questions/6906916/collisions-when-generating-uuids-in-javascript

      // generate some random numbers
      var nums = new Array(31);

      if (window.crypto && crypto.getRandomValues) {
        // WHATWG crypto-based RNG - http://wiki.whatwg.org/wiki/Crypto
        // Moderately fast, high quality
        var _rnds8 = new Uint8Array(31);
        crypto.getRandomValues(_rnds8);
        for (var i = 0; i < 31; i++) {
          nums[i] = _rnds8[i] & 0xf;
        }
      }
      else {
        // Math.random()-based (RNG)
        for (var i = 0, r; i < 31; i++) {
          nums[i] = Math.random()*16|0;
        };
      }

      var p = 0;
      return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        var r = nums[p++], v = c == 'x' ? r : (r&0x3|0x8);
        return v.toString(16);
      });
    },

    /**
     * Generate a deviceId from an uuid.
     * @private
     * @return {string} deviceId
     */
    getDeviceId: function() {
      var deviceId;
      var key = "deviceId";
      if (window.localStorage) deviceId = localStorage.getItem(key);
      if (!deviceId) {
        deviceId = this.getUUID() + '-' + (+new Date()).toString(16);
        if (window.localStorage) localStorage.setItem(key, deviceId);
      }
      return deviceId;
    },

    /**
     * Dispatch an event on an element.
     * @private
     * @param DOMElement element
     * @param {string} eventType
     * @param {object} data
     * @return boolean success
     */
    fireEvent: function(element, eventType, data) {
      var evt;
      if(document.createEventObject)
      {
        // dispatch for IE
        evt = document.createEventObject();
        evt.data = data;
        return element.fireEvent('on'+eventType,evt);
      }
      else
      {
        evt = new CustomEvent(eventType, {detail:data, bubbles:true, cancelable:true});
        return !element.dispatchEvent(evt);
      }
    }

  };

  /**
   * Scoreflex SDK Object.
   * @param {string} clientId
   * @param {string} clientSecret
   * @param {boolean} useSandbox
   *
   * @public
   * @namespace SDK
   * @memberof module:Scoreflex
   */
  SFX.SDK = (function(clientId, clientSecret, useSandbox) {
    var DEFAULT_LANGUAGE_CODE = "en";
    var VALID_LANGUAGE_CODES = ["af", "ar", "be",
      "bg", "bn", "ca", "cs", "da", "de", "el", "en", "en_GB", "en_US", "es",
      "es_ES", "es_MX", "et", "fa", "fi", "fr", "fr_FR", "fr_CA", "he", "hi",
      "hr", "hu", "id", "is", "it", "ja", "ko", "lt", "lv", "mk", "ms", "nb",
      "nl", "pa", "pl", "pt", "pt_PT", "pt_BR", "ro", "ru", "sk", "sl", "sq",
      "sr", "sv", "sw", "ta", "th", "tl", "tr", "uk", "vi", "zh", "zh_CN",
      "zh_TW", "zh_HK"];

    var _initialized = false;
    var _initState = Scoreflex.SessionState.INIT_UNSTARTED;

    var _context = {
      clientId:null,
      clientSecret:null,
      useSandbox:true
    };
    var _session = {
      accessToken:null,
      sid:null,
      playerId:null
    };


    //-- Common
    /**
     * Namespace: Common
     * @private
     * @namespace Common
     * @memberof module:Scoreflex.SDK
     */
    var Common = (function() {
      /**
       * Get an XMLHttpRequest2 object supporting CORS.
       * @param {string} method
       * @param {string} url
       * @return {XMLHttpRequest}
       *
       * @private
       * @memberof module:Scoreflex.SDK.Common
       */
      var getXHR = function(method, url) {
        var xhr = new XMLHttpRequest();
        if ("withCredentials" in xhr) {
          // XHR for Chrome/Firefox/Opera/Safari.
          xhr.open(method, url, true);
        }
        else if (typeof XDomainRequest != "undefined") {
          // XDomainRequest for IE.
          xhr = new XDomainRequest();
          xhr.open(method, url);
        }
        else {
          // CORS not supported.
          xhr = null;
        }
        return xhr;
      };

      /**
       * Ajax call.
       * @param {string} method
       * @param {string} url
       * @param {object} params in query string
       * @param {mixed} body (not supported yet)
       * @param {object} headers
       * @param {module:Scoreflex.SDK.Handlers} handlers
       *
       * @private
       * @memberof module:Scoreflex.SDK.Common
       */
      var request = function(method, url, params, body, headers, handlers) {
        var xhr = getXHR(method, url);
        if (headers !== undefined) {
          for (var h in headers) {
            xhr.setRequestHeader(h, headers[h]);
          }
        }
        if (xhr) {
          if (handlers) {
            var h = ['onerror', 'ontimeout', 'onabort', 'onloadend'];
            for (var i=0; i<h.length; i++) {
              if (handlers[h[i]]) {
                xhr[h[i]] = handlers[h[i]];
              }
            }
          }
          xhr.onload = function() {
            if (handlers) {
              try {
                xhr.responseJSON = parseJSON(this.responseText);
              }
              catch(e){}
              var status = this.status;
              if (status == 200 && handlers.onload) {
                handlers.onload.apply(this, arguments);
              }
              else if (handlers.onerror) {
                handlers.onerror.apply(this, arguments);
              }
            }
          };
          if (method === 'POST') {
            xhr.send(params);
          }
          else {
            xhr.send();
          }
        }
      };

      /**
       * Sort list of objects.
       * @param {object} a
       * @param {object} b
       * @return {int}
       *
       * @private
       * @memberof module:Scoreflex.SDK.Common
       */
      var _sortParams = function(a, b) {
        if (a.k == b.k) {
          return a.v < b.v ? -1 : 1;
        }
        return a.k < b.k ? -1 : 1;
      };

      /**
       * Generate the signature for ajax call.
       * @param {string} method
       * @param {string} url
       * @param {object} params
       * @param {string} body
       * @return {string} signature
       *
       * @private
       * @memberof module:Scoreflex.SDK.Common
       */
      var getSignature = function(method, url, params, body) {
        if (body === undefined) body = '';
        if (params === undefined) params = [];
        var i;

        // parse url
        var link = document.createElement('a');
        link.href = url;
        var protocol = link.protocol.substr(0, link.protocol.length-1).toLowerCase();
        var host = link.hostname.toLowerCase();
        var path = link.pathname;
        var queryString = link.search;
        if (queryString.length > 0 && queryString[0] === '?') {
          queryString = queryString.substr(1);
        }
        // params from url (already encoded)
        var qsp = queryString.split('&');
        var eparams = [];
        var kv;
        for (i=0; i<qsp.length; i++) {
          if (qsp[i] !== '') {
            kv = qsp[i].split('=', 2);
            eparams.push({k:kv[0], v:kv[1]});
          }
        }

        var encode = SFX.Helper.rawurlencode;

        // additionnal params (to encode)
        for (var k in params) {
          eparams.push({k:encode(k), v:encode(params[k])});
        }
        // sort all params
        eparams.sort(_sortParams); // sort by keys
        // concatenate sorted params
        var cparams = [];
        for (i=0; i<eparams.length; i++) {
          cparams.push(eparams[i].k + '=' + eparams[i].v);
        }
        // final query string
        var qs = cparams.join('&');

        var string = method
                   + '&'
                   + encode(protocol + '://' + host + path)
                   + '&'
                   + encode(qs)
                   + '&'
                   + encode(body);

        var secret = getContext().clientSecret;
        var hash = CryptoJS.HmacSHA1(string, secret);
        var hashB64 = hash.toString(CryptoJS.enc.Base64);
        var sig = encode(hashB64);
        return 'Scoreflex sig="'+sig+'", meth="0"';
      };

      /**
       * Turn a JSON-string into a JavaScript object.
       * @param {string} text
       * @return {object}
       *
       * @private
       * @memberof module:Scoreflex.SDK.Common
       */
      var parseJSON = function(text) {
        try {
          var json = JSON.parse(text);
          return json;
        }
        catch(e) {}
        return null;
      };

      return {
        request:request,
        getSignature:getSignature
      };

    })();
    //-- Common end


    //-- Handlers
    /**
     * Scoreflex object to handle RestClient request responses
     * @callback module:Scoreflex.SDK.Handlers
     * @param {object} callbacks
     * Callback functions are called in the scope (this keyword) of the
     * XMLHttpRequest object when available or the window object otherwise.
     * <br />- onload (Function) called on request success
     * <br />- onerror (Function) called on request failure
     * <br />- ontimeout (Function)
     * <br />- onabort (Function)
     * <br />- onloadend (Function)
     * @see See also {@link http://www.w3.org/TR/XMLHttpRequest2/#events|XMLHttpRequest2}
     */
    //-- Handlers end


    //-- Events
    /**
     * ScoreflexEvent
     * @protected
     * @namespace Events
     * @memberof module:Scoreflex.SDK
     */
    var Events = {
      /**
       * @return {ScoreflexEvent}
       * @private
       */
      ScoreflexSessionEvent: function() {
        /**
         * ScoreflexEvent to indicate SDK initialization state.
         * @property {string} name "session"
         * @property {module:Scoreflex.SessionState} state The sesssion initialization state
         *
         * @event ScoreflexSessionEvent
         * @memberof module:Scoreflex.SDK.Events
         * @see {@link module:Scoreflex.SDK.getSessionState}
         */
        return {
          name: "session",
          state: getSessionState()
        };
      },

      /**
       * @return {ScoreflexEvent}
       * @private
       */
      ScoreflexPlayerEvent: function() {
        var s = getSession() || {};
        /**
         * ScoreflexEvent to indicate the current player has been set or reset.
         * @property {string} name "player"
         * @property {module:Scoreflex.SDK.Player} player The current player
         * @property {boolean} anonymous "true" if the player is anonymous, "false" otherwise.
         *
         * @event ScoreflexPlayerEvent
         * @memberof module:Scoreflex.SDK.Events
         * @see {@link module:Scoreflex.SDK.Players.getCurrent}
         */
        return {
          name: "player",
          player:s.me,
          anonymous:s.anonymous
        };
      },

      /**
       * @param {module:Scoreflex.SDK.Leaderboard} leaderboard - Leaderboard instance
       * @return {ScoreflexEvent}
       * @private
       */
      ScoreflexPlayEvent: function(leaderboard) {
        /**
         * ScoreflexEvent to indicate the player wants to play a leaderboard.
         * The game associated to the leaderboard should be started.
         * @property {string} name "play"
         * @property {module:Scoreflex.SDK.Leaderboard} leaderboard The Leaderboard object
         *
         * @event ScoreflexPlayEvent
         * @memberof module:Scoreflex.SDK.Events
         */
        return {
          name: "play",
          leaderboard: leaderboard
        };
      },

      /**
       * @param {module:Scoreflex.SDK.ChallengeInstance} challenge - ChallengeIntance instance
       * @return {ScoreflexEvent}
       * @private
       */
      ScoreflexChallengeEvent: function(challenge) {
        /**
         * ScoreflexEvent to indicate the player wants to play a challenge.
         * The game associated with the challenge should be started.
         * @property {string} name "challenge"
         * @property {module:Scoreflex.SDK.ChallengeInstance} challenge The Challenge object
         *
         * @event ScoreflexChallengeEvent
         * @memberof module:Scoreflex.SDK.Events
         */
        return {
          name: "challenge",
          challenge: challenge
        };
      },

      /**
       * Fire a ScoreflexEvent in the window object.
       * @param {object} eventData
       *
       * @private
       * @memberof module:Scoreflex.SDK.Events
       */
      fire: function(eventData) {
        SFX.Helper.fireEvent(window, 'ScoreflexEvent', eventData);
      }
    };
    //-- Events end


    //-- REST API
    /**
     * Namespace: RestClient
     * @protected
     * @namespace RestClient
     * @memberof module:Scoreflex.SDK
     */
    var RestClient = (function() {
      /*
       * CONSTANTS
       */
      var API_VERSION = 'v1';
      var PRODUCTION_API_URL = 'https://api.scoreflex.com';
      var SANDBOX_API_URL = 'https://sandbox.api.scoreflex.com';

      /**
       * Get the url to reach REST API endpoint.
       * @param {string} path - API endpoint path
       * @return {string} url
       *
       * @private
       * @memberof module:Scoreflex.SDK.RestClient
       */
      var getUrl = function(path) {
        return (getContext().useSandbox ? SANDBOX_API_URL : PRODUCTION_API_URL)
                + '/' + API_VERSION
                + path;
      };

      /**
       * Add parameters common to all REST API calls.
       * @param {object} params - key/value pair of query string parameters
       * @return {object} all params
       *
       * @private
       * @memberof module:Scoreflex.SDK.RestClient
       */
      var addCommonParams = function(params) {
        if (params === undefined) params = {};
        var session = getSession() || {};
        var lang = session.lang || window.navigator.language;
        params.lang = SFX.Helper.inArray(lang, VALID_LANGUAGE_CODES) ? lang : 'en';
        if (session.accessToken) {
          params.accessToken = session.accessToken;
        }
        return params;
      };


      /**
       * Turn a list of object parameters to a query string.
       * @param {object} params - key/value pair of query string parameters
       * @return {string} query string
       *
       * @private
       * @memberof module:Scoreflex.SDK.RestClient
       */
      var paramsToQueryString = function(params) {
        var p = [];
        for (var k in params) {
          p.push(encodeURIComponent(k) + '=' + encodeURIComponent(params[k]));
        }
        return p.join('&');
      };

      /**
       * Perform a GET request to Scoreflex REST API.
       * @param {string} path - API endpoint path
       * @param {object} params - key/value pair of query string parameters
       * @param {module:Scoreflex.SDK.Handlers} handlers - request callbacks
       *
       * @public
       * @memberof module:Scoreflex.SDK.RestClient
       */
      var get = function(path, params, handlers) {
        params = addCommonParams(params);
        var url = getUrl(path);
        var parameters = paramsToQueryString(params);
        if (parameters) {
          url = url + '?' + parameters;
        }
        var headers = {};
        var body = undefined;
        Common.request('GET', url, {}, body, headers,handlers);
      };

      /**
       * Perform a POST request to Scoreflex REST API.
       * @param {string} path - API endpoint path
       * @param {object} params - key/value pair of query string parameters
       * @param {mixed} body - (not implemented)
       * @param {module:Scoreflex.SDK.Handlers} handlers - request callbacks
       *
       * @public
       * @memberof module:Scoreflex.SDK.RestClient
       */
      var post = function(path, params, body, handlers) {
        params = addCommonParams(params);
        var url = getUrl(path);
        var parameters = paramsToQueryString(params);
        var headers = {
          "Content-type": "application/x-www-form-urlencoded",
          "X-Scoreflex-Authorization": Common.getSignature('POST', url, params, body)
        };
        Common.request('POST', url, parameters, body, headers, handlers);
      };

      return{
        post:post,
        get:get
      };
    })();
    //-- REST API end


    //-- WEB API
    /**
     * Namespace: WebClient
     * @protected
     * @namespace WebClient
     * @memberof module:Scoreflex.SDK
     */
    var WebClient = (function(){

      /*
       * CONSTANTS
       */
      var API_VERSION = 'v1';
      var PRODUCTION_WEBAPI_URL = 'https://api.scoreflex.com';
      var SANDBOX_WEBAPI_URL = 'https://sandbox.api.scoreflex.com';

      /*
       * Variables
       */
      var iframes = {};
      var iframesW = {};
      var stackIds = [];

      /**
       * Catch WebClient event messages. Apply action or dispatch local event.
       * @param {object} params - parameters of the event sent
       * @param {string} iframeId - identifier of the WebClient iframe sending the event
       *
       * @private
       * @memberof module:Scoreflex.SDK.WebClient
       */
      var handleCallback = function(params, iframeId) {
        var code = params.code;
        var data = params.data ? JSON.parse(decodeURIComponent(params.data)) : {};

        switch (code) {
          case '200000': // logout
            close(iframeId);
            setSession(null, true);
            fetchAnonymousAccessTokenIfNeeded();
            break;

          case '200001': // close webview
            close(iframeId);
            break;

          case '200002': // play leaderboard
            var leaderboardId = data.leaderboardId;
            Events.fire(Events.ScoreflexPlayEvent(Leaderboards.get(leaderboardId)));
            close(iframeId);
            break;

          case '200003': // need authentication
            close(iframeId);
            authorize();
            break;

          case '200004': // oauth authentication granted
            var oauthCode = data.code;
            var oauthState = data.state;
            close(iframeId);
            fetchAccessToken({code:oauthCode, state:oauthState});
            break;

          case '200005': // move to new url
            var url = data.url;
            var mode = data.mode; // full or panel
            close(iframeId);
            show(url, {}, {style:mode});
            break;

          case '200006': // authenticate with service
            var service = data.service;
            var nextUrl = data.nextUrl;
            // TODO
            close(iframeId);
            break;

          case '200007': // launch challenge
            var challengeInstanceId = data.challengeInstanceId;
            var challengeConfigId = data.challengeConfigId;
            close(iframeId);
            Events.fire(Events.ScoreflexChallengeEvent(Challenges.get(challengeInstanceId, challengeConfigId)));
            break;

          case '200008': // link with service
            // TODO
            close(iframeId);
            break;

          case '200009': // invite with service
            // TODO
            close(iframeId);
            break;

          case '200010': // share with service
            // TODO
            close(iframeId);
            break;
        }
      };

      /**
       * Apply a style to a WebClient iframe.
       * @param {DOMIframeElement} iframe
       * @param {object} opt
       *
       * @private
       * @memberof module:Scoreflex.SDK.WebClient
       */
      var applyStyle = function(iframe, opt) {
        var styleName = "scoreflexWebClient_"+opt.style;
        if (styleName !== undefined) {
          var curStyleName = iframe.getAttribute('data-stylename');
          if (curStyleName !== styleName) {
            iframe.classList.remove(curStyleName);
            iframe.classList.add(styleName);
            iframe.setAttribute('data-stylename', styleName);
          }
        }
      };

      /**
       * Get a reference to a WebClient iframe.
       * @param {string} id - reference name of the iframe
       * @return {DOMIframeElement}
       *
       * @private
       * @memberof module:Scoreflex.SDK.WebClient
       */
      var getIframe = function(id) {
        if (!iframes[id]) {
          var iframe = document.createElement('iframe');
          iframe.id = "scoreflexWebClient_"+id;
          iframes[id] = iframe;
          iframe.onload = function() {
            iframesW[id]= iframe.contentWindow;
          };
        }
        return iframes[id];
      };

      /**
       * Set an iframe as the last opened/used one.
       * @param {string} id
       *
       * @private
       * @memberof module:Scoreflex.SDK.WebClient
       */
      var stackTop = function(id) {
        stackRemove(id);
        stackIds.push(id);
      };
      /**
       * Remove an iframe from the opened/used stack.
       * @param {string} id
       *
       * @private
       * @memberof module:Scoreflex.SDK.WebClient
       */
      var stackRemove = function(id) {
        for (var i=0; i<stackIds.length; i++) {
          if (stackIds[i] === id) {
            stackIds.splice(i, 1);
            break;
          }
        }
      };
      /**
       * Get the last opened/used iframe id.
       * @return {string|null} iframe id
       *
       * @private
       * @memberof module:Scoreflex.SDK.WebClient
       */
      var getStackTopId = function() {
        if (stackIds.length > 0) {
          return stackIds[stackIds.length-1];
        }
        return null;
      };

      var lastUrlHandled = null;
      /**
       * Internal callback for WebClient events.
       * @param {DOMMessageEvent} event
       *
       * @private
       * @memberof module:Scoreflex.SDK.WebClient
       */
      var onUrlChange = function(event) {
        if (event.origin == PRODUCTION_WEBAPI_URL || event.origin == SANDBOX_WEBAPI_URL) {
          var url = event.data;
          if (url === lastUrlHandled) {
            // we avoid to handled twice the same url
            return;
          }
          lastUrlHandled = url;
          var link = document.createElement('a');
          link.href = url;
          if (link.pathname == '/v1/web/callback') {
            var qs = link.search;
            if (qs.length > 0 && qs[0] == '?') qs = qs.substr(1);
            var list = qs.split('&');
            var params = {};
            var kv, i, k;
            for (i=0; i<list.length; i++) {
              if (list[i]) {
                kv = list[i].split('=', 2);
                params[kv[0]] = kv[1];
              }
            }
            // find the right iframe
            var iframeId = null;
            for (k in iframesW) {
              if (event.source === iframesW[k]) {
                iframeId = k;
                break;
              }
            }
            handleCallback(params, iframeId);
          }
        }
      };

      window.addEventListener("message", onUrlChange, false);

      /**
       * Add parameters common to all WEB API calls.
       * @param {object} params
       * @param {object} options (.noSid)
       * @return {object} all params
       *
       * @private
       * @memberof module:Scoreflex.SDK.WebClient
       */
      var addCommonParams = function(params, options) {
        if (params === undefined) params = {};
        if (options === undefined) options = {};
        var session = getSession() || {};
        var lang = session.lang || window.navigator.language;
        params.lang = SFX.Helper.inArray(lang, VALID_LANGUAGE_CODES) ? lang : 'en';
        if (session.sid && !options.noSid) {
          params.sid = session.sid;
        }
        return params;
      };

      /**
       * Merge WebClient options.
       * @param {object} options
       * @param {object} defaultOptions
       * @return {object} merged options
       *
       * @private
       * @memberof module:Scoreflex.SDK.WebClient
       */
      var mergeOptions = function(options, defaultOptions) {
        var opt = options || {};
        if (defaultOptions !== undefined) {
          for (var k in defaultOptions) {
            if (opt[k] === undefined) {
              opt[k] = defaultOptions[k];
            }
          }
        }
        return opt;
      };

      /**
       * Build the url for the WebClient.
       * @param {string} path
       * @param {object} params - query string parameters
       * @param {object} options - query string options (.noSid)
       * @return {string|null} url
       *
       * @private
       * @memberof module:Scoreflex.SDK.WebClient
       */
      var getUrl = function(path, params, options) {
        if (path.indexOf('://') !== -1) {
          // we have an absolute url. No process required
          return path;
        }
        var useSandbox = getContext().useSandbox;
        var sid = getSession().sid;
        if (sid) {
          params = addCommonParams(params, options);
          var eparams = [];
          for (var k in params) {
            eparams.push(encodeURIComponent(k)+'='+encodeURIComponent(params[k]));
          }
          var qs = eparams.join('&');

          return (useSandbox ? SANDBOX_WEBAPI_URL : PRODUCTION_WEBAPI_URL)
                + '/' + API_VERSION + path
                + '?' + qs;
        }
        return null;
      };

      /**
       * Display a WebClient.
       * @param {string} path - API endpoint path
       * @param {object} params - key/value pair of query string parameters
       * @param {object} options - query string options and WebClient options
       * @param {object} defaultOptions - default WebClient style options
       *
       * @public
       * @memberof module:Scoreflex.SDK.WebClient
       */
      var show = function(path, params, options, defautOptions) {
        var opt = mergeOptions(options, defautOptions);
        var url = getUrl(path, params, opt);
        if (url) {
          var id = opt.id || opt.style || 'full';
          var iframe = getIframe(id);
          iframe.src = url + '#start';
          applyStyle(iframe, opt);
          document.body.appendChild(iframe);
          stackTop(id);
        }
        else {
          close();
        }
      };

      /**
       * Close the WebClient iframe identified by iframeId, or the
       * last opened/used one.
       * @param {string} iframeId
       *
       * @public
       * @memberof module:Scoreflex.SDK.WebClient
       */
      var close = function(iframeId) {
        var iframe;
        if (!iframeId) {
          // if iframeId is not defined, find the stack-top id
          iframeId = getStackTopId();
        }
        if (iframeId) {
          iframe = getIframe(iframeId);
          if (iframe && iframe.parentNode) {
            iframe.parentNode.removeChild(iframe);
            stackRemove(iframeId);
          }
        }
      };

      return {
        show:show,
        close:close
      };
    })();
    //-- WEB API end


    //-- STORAGE
    /**
     * Storage using localStorage.
     * @private
     * @namespace Storage
     * @memberof module:Scoreflex.SDK
     */
    var Storage = (function(){
      var _ns = 'SFX_' + clientId + '_' + (useSandbox ? '1' : '0');
      /**
       * Get an object by key.
       * @param {string} key
       * @return {object}
       *
       * @private
       * @memberof module:Scoreflex.SDK.Storage
       */
      var get = function(key) {
        var s = localStorage.getItem(_ns + key);
        if (s) {
          return JSON.parse(s);
        }
        return null;
      };
      /**
       * Associate an object to a key.
       * @param {string} key
       * @param {object} data
       *
       * @private
       * @memberof module:Scoreflex.SDK.Storage
       */
      var set = function(key, data) {
        var s = JSON.stringify(data);
        return localStorage.setItem(_ns + key, s);
      };
      /**
       * Remove object associated with a key.
       * @param {string} key
       *
       * @private
       * @memberof module:Scoreflex.SDK.Storage
       */
      var rm = function(key) {
        return localStorage.removeItem(_ns + key);
      };

      return {
        get:get,
        set:set,
        rm:rm
      };
    })();
    //-- STORAGE end


    //-- Player object
    /**
     * Player instance object.
     * @param {string} playerId - player ID
     * @param {object} data - player raw data
     *
     * @public
     * @class Player
     * @memberof module:Scoreflex.SDK
     */
    var Player = function(playerId, data) {
      /**
       * Return the player's identifier (playerId).
       * @return {string} Player ID
       *
       * @public
       * @instance
       * @memberof module:Scoreflex.SDK.Player
       */
      var getId = function() {
        return playerId;
      };

      /**
       * Return the player raw data.
       * @return {object}
       *
       * @public
       * @instance
       * @memberof module:Scoreflex.SDK.Player
       */
      var getData = function() {
        return data || {};
      };

      /**
       * Set the player raw data.
       *
       * @public
       * @instance
       * @memberof module:Scoreflex.SDK.Player
       */
      var setData = function(newData) {
        data = newData;
      };

      /**
       * Get a player's data key
       * @return {mixed}
       *
       * @public
       * @instance
       * @memberof module:Scoreflex.SDK.Player
       */
      var getValue = function(key) {
        var d = getData();
        return d[key];
      };

      /**
       * Return the player's nickname.
       * @return {string} nickname
       *
       * @public
       * @instance
       * @memberof module:Scoreflex.SDK.Player
       */
      var getNickname = function() {
        return getValue("nickName") || '';
      };

      /**
       * Return the avatar url of the player
       * @return {string} url
       *
       * @public
       * @instance
       * @memberof module:Scoreflex.SDK.Player
       */
      var getAvatarUrl = function() {
        var url = getValue("avatarUrl");
        if (url) {
          return url;
        }
        return "https://www.scoreflex.com/"
                + (getContext().useSandbox ? 'sandbox/' : '')
                + "avatars/players/"+ getId() +"/";
      };

      /**
       * Return the location Object for the player, or a field value
       * @param {string} [key] - facultative field key (id, adminLevel, countryCode, formatted or title)
       * @return {Object|string}
       *
       * @public
       * @instance
       * @memberof module:Scoreflex.SDK.Player
       */
      var getGeo = function(key) {
        var geo = getValue("geo") || {};
        if (key !== undefined) {
          return geo[key] || "";
        }
        return geo;
      };

      /**
       * Display a web client with the profile of the player.
       * @param {object} parameters - key/value pair of query string parameters
       * @param {object} options - key/value pair of WebClient options
       *
       * @public
       * @instance
       * @memberof module:Scoreflex.SDK.Player
       */
      var showProfile = function(parameters, options) {
        Players.showProfile(playerId, parameters, options);
      };

      /**
       * Display a web client with the list of friends of the player.
       * @param {object} parameters - key/value pair of query string parameters
       * @param {object} options - key/value pair of WebClient options
       *
       * @public
       * @instance
       * @memberof module:Scoreflex.SDK.Player
       */
      var showFriends = function(parameters, options) {
        showPlayerFriends(playerId, parameters, options);
      };

      return {
        getId:getId,
        getData:getData,
        setData:setData,
        getValue:getValue,
        getNickname:getNickname,
        getAvatarUrl:getAvatarUrl,
        getGeo:getGeo,
        showProfile:showProfile,
        showFriends:showFriends
      };
    };
    //-- Player object end


    //-- Players static
    /**
     * Object to get and manipulate players.
     * @public
     * @namespace Players
     * @memberof module:Scoreflex.SDK
     */
    var Players = (function() {
      var cache = {};

      /**
       * Return the current player as a Player object.
       * @return {module:Scoreflex.SDK.Player} Player instance
       *
       * @public
       * @memberof module:Scoreflex.SDK.Players
       */
      var getCurrent = function() {
        var data = (getSession() || {}).me || {};
        var playerId = data.id;
        return Player(playerId, data);
      };

      /**
       * Request a player and call the onload handler with a {@link module:Scoreflex.SDK.Player}
       * instance.
       * @param {string} playerId - Player ID
       * @param {object} parameters - key/value pair of query string parameters
       * @param {module:Scoreflex.SDK.Handlers} handlers - request callbacks
       * @param {boolean} noCache - if true, bypass the local cache
       *
       * @public
       * @memberof module:Scoreflex.SDK.Players
       */
      var get = function(playerId, parameters, handlers, noCache) {
        if (!isInitialized()) return;
        if (handlers === undefined) handlers = {};

        if (!noCache && cache[playerId]) {
          if (handlers.onload) {
            var p = Player(playerId, cache[playerId]);
            setTimeout(function(){handlers.onload.call(null, p);}, 0);
          }
        }
        else {
          var i_onload = handlers.onload;
          handlers.onload = function() {
            var json = this.responseJSON || {};
            cache[playerId] = json;
            var p = Player(playerId, json);
            i_onload.call(this, p);
          };

          RestClient.get("/players/"+playerId, parameters, handlers);
        }
      };

      /**
       * Display a web client with the profile of a player (default, current player).
       * @param {string} playerId - default 'me'
       * @param {object} parameters - key/value pair of query string parameters
       * @param {object} options - key/value pair of WebClient options
       *
       * @public
       * @memberof module:Scoreflex.SDK.Players
       */
      var showProfile = function(playerId, parameters, options) {
        if (!isInitialized()) return;
        if (playerId === undefined) playerId = 'me';
        var params = pushParameters({}, parameters);
        var defaultOpt = {style:'full'};
        WebClient.show("/web/players/"+playerId, params, options, defaultOpt);
      };

      /**
       * Display a web client with the list of friends of a player (default, current player).
       * @param {string} playerId - default 'me'
       * @param {object} parameters - key/value pair of query string parameters
       * @param {object} options - key/value pair of WebClient options
       *
       * @public
       * @memberof module:Scoreflex.SDK.Players
       */
      var showFriends = function(playerId, parameters, options) {
        if (!isInitialized()) return;
        if (playerId === undefined) playerId = 'me';
        var params = pushParameters({}, parameters);
        var defaultOpt = {style:'full'};
        WebClient.show("/web/players/"+playerId+"/friends", params, options, defaultOpt);
      };

      return  {
        get:get,
        getCurrent:getCurrent,
        showProfile:showProfile,
        showFriends:showFriends
      };
    })();
    //-- Players static end


    //-- Leaderboard object
    /**
     * Leaderboard instance object.
     * @param {string} leaderboardId - Leaderboard ID
     *
     * @public
     * @class Leaderboard
     * @memberof module:Scoreflex.SDK
     */
    var Leaderboard = function(leaderboardId) {
      /**
       * Return the leaderboard identifier (leaderboardId).
       * @return {string} Leaderboard ID
       *
       * @public
       * @instance
       * @memberof module:Scoreflex.SDK.Leaderboard
       */
      var getId = function() {
        return leaderboardId;
      };

      /**
       * Send a score to a leaderboard.
       * @param {int} score - raw score
       * @param {object} parameters - key/value pair of query string parameters
       * @param {module:Scoreflex.SDK.Handlers} handlers - request callbacks
       *
       * @public
       * @instance
       * @memberof module:Scoreflex.SDK.Leaderboard
       */
      var submitScore = function(score, parameters, handlers) {
        if (!isInitialized()) return;
        var params = {score:score};
        params = pushParameters(params, parameters);
        var body = undefined;
        RestClient.post("/scores/"+leaderboardId, params, body, handlers);
      };

      /**
       * Display a web client for the requested leaderboard.
       * @param {object} parameters - key/value pair of query string parameters
       * @param {object} options - key/value pair of WebClient options
       *
       * @public
       * @instance
       * @memberof module:Scoreflex.SDK.Leaderboard
       */
      var show = function(parameters, options) {
        if (!isInitialized()) return;
        var params = pushParameters({}, parameters);
        var defaultOpt = {style:'full'};
        WebClient.show("/web/leaderboards/"+leaderboardId, params, options, defaultOpt);
      };

      /**
       * Display a web client for the requested leaderboard overview.
       * @param {object} parameters - key/value pair of query string parameters
       * @param {object} options - key/value pair of WebClient options
       *
       * @public
       * @instance
       * @memberof module:Scoreflex.SDK.Leaderboard
       */
      var showOverview = function(parameters, options) {
        if (!isInitialized()) return;
        var params = pushParameters({}, parameters);
        var defaultOpt = {style:'full'};
        WebClient.show("/web/leaderboards/"+leaderboardId+"/overview", params, options, defaultOpt);
      };

      /**
       * Display a web client with the current player's score for the requested
       * leaderboard.
       * @param {object} parameters - (facultative, the last score of the player} 'score':{int}
       * @param {object} options - key/value pair of WebClient options
       *
       * @public
       * @instance
       * @memberof module:Scoreflex.SDK.Leaderboard
       */
      var showRankbox = function(parameters, options) {
        if (!isInitialized()) return;
        var params = pushParameters({}, parameters);
        var defaultOpt = {style:'panel'};
        WebClient.show("/web/scores/"+leaderboardId+"/ranks", params, options, defaultOpt);
      };

      /**
       * Put a score and show the rankbox. The rankbox takes into account
       * the score.
       * @param {int} score - raw score
       * @param {object} parameters - key/value pair of query string parameters
       * @param {object} options - key/value pair of WebClient options
       *
       * @public
       * @instance
       * @memberof module:Scoreflex.SDK.Leaderboard
       */
      var submitScoreAndShowRankbox = function(score, parameters, options) {
        submitScore(score, parameters);
        var params = parameters || {};
        params.score = score;
        showRankbox(params, options);
      };

      return {
        getId:getId,
        submitScore:submitScore,
        show:show,
        showOverview:showOverview,
        showRankbox:showRankbox,
        submitScoreAndShowRankbox:submitScoreAndShowRankbox
      };
    };
    //-- Leaderboard object end


    //-- Leaderboards static
    /**
     * Object to get and manipulate leaderboards.
     * @public
     * @namespace Leaderboards
     * @memberof module:Scoreflex.SDK
     */
    var Leaderboards = (function() {
      /**
       * Get a Leaderboard instance
       * @return {module:Scoreflex.SDK.Leaderboard} Leaderboard instance
       *
       * @public
       * @memberof module:Scoreflex.SDK.Leaderboards
       */
      var get = function(leaderboardId) {
        return Leaderboard(leaderboardId);
      };

      return {
        get:get
      };
    })();
    //-- Leaderboards static end


    //-- ChallengeInstance object
    /**
     * ChallengeInstance object.
     * @param {string} instanceId - Challenge instance ID
     * @param {string} configId - Challenge configuration ID
     *
     * @public
     * @class ChallengeInstance
     * @memberof module:Scoreflex.SDK
     */
    var ChallengeInstance = function(instanceId, configId) {
      var cache_players = null;

      /**
       * Return the challenge instance identifier (challengeInstanceId).
       * @return {string} Challenge instance ID
       *
       * @public
       * @instance
       * @memberof module:Scoreflex.SDK.ChallengeInstance
       */
      var getInstanceId = function() {
        return instanceId;
      };

      /**
       * Return the challenge configuration identifier of the
       * instance (challengeConfigId).
       * @return {string} Challenge configuration ID
       *
       * @public
       * @instance
       * @memberof module:Scoreflex.SDK.ChallengeInstance
       */
      var getConfigId = function() {
        return configId;
      };

      /**
       * Get details of the challenge.
       * @param {object} parameters - key/value pair of query string parameters
       * @param {module:Scoreflex.SDK.Handlers} handlers - request callbacks
       *
       * @public
       * @instance
       * @memberof module:Scoreflex.SDK.ChallengeInstance
       */
      var getDetails = function(parameters, handlers) {
        if (!isInitialized()) return;
        var params = pushParameters(params, parameters);
        RestClient.get("/challenges/instances/"+instanceId, params, handlers);
      };

      /**
       * Request challenge players and call the onload handler with a
       * list of {@link Player}.
       * @param {object} parameters - key/value pair of query string parameters
       * @param {module:Scoreflex.SDK.Handlers} handlers - request callbacks
       * @param {boolean} noCache - if true, bypass the local cache
       *
       * @public
       * @instance
       * @memberof module:Scoreflex.SDK.ChallengeInstance
       */
      var getPlayers = function(parameters, handlers, noCache) {
        if (handlers === undefined) handlers = {};
        if (!noCache && cache_participants !== null) {
          if (handlers.onload) {
            setTimeout(function(){handlers.onload.call(null, cache_players);}, 0);
          }
        }
        else {
          var detailsHandlers = {
            onload: function() {
              var cjson = this.responseJSON || {};
              var participants = cjson.participants || {};
              var players = {};
              var count = 0;
              var received = 0;
              var id;
              // prebuild Player objects without data
              for (id in participants) {
                count++;
                players[id] = Player(id, {});
              }
              var incrementalHandler = function(pjson) {
                received++;
                if (pjson.id) {
                  // update Player object with data
                  players[pjson.id] = Player(pjson.id, pjson);
                }
                if (received >= count) {
                  cache_players = players;
                  setTimeout(function(){handlers.onload.call(null, players);}, 0);
                }
              };
              for (id in participants) {
                Players.get(id, {}, {
                  onload: function() {
                    var pjson = this.responseJSON || {};
                    incrementalHandler(pjson);
                  }
                });
              }
            },
            onerror: function() {
              if (handlers.onerror) handler.onerror.apply(this, arguments);
            }
          };
          getDetails(parameters, detailsHandlers);
        }
      };

      /**
       * Get turn details of a challenge instance.
       * @param {object} parameters - key/value pair of query string parameters
       * @param {module:Scoreflex.SDK.Handlers} handlers - request callbacks
       *
       * @public
       * @instance
       * @memberof module:Scoreflex.SDK.ChallengeInstance
       */
      var getTurns = function(parameters, handlers) {
        if (!isInitialized()) return;
        var params = pushParameters(params, parameters);
        RestClient.get("/challenges/instances/"+instanceId+"/turns", params, handlers);
      };

      /**
       * Generic function to submit a challenge player's turn.
       * @param {object} turnBody - {@link http://developer.scoreflex.com/docs}
       * @param {object} parameters - key/value pair of query string parameters
       * @param {module:Scoreflex.SDK.Handlers} handlers - request callbacks
       *
       * @todo set link to challenge's turns documentation
       * @public
       * @instance
       * @memberof module:Scoreflex.SDK.ChallengeInstance
       */
      var submitTurn = function(turnBody, parameters, handlers) {
        if (!isInitialized()) return;
        if (!turnBody || turnBody.turnSequence === undefined) {
          // request the turnSequence if we don't have it
          getDetails({fields:"turn"}, {
            onload: function() {
              var json = this.responseJSON || {};
              var turnSequence = (json.turn || {}).sequence || 0;
              var newTurnBody = turnBody || {};
              newTurnBody.turnSequence = turnSequence;
              submitTurn(newTurnBody, parameters, handlers);
            },
            onerror: (handlers || {}).onerror
          });
        }
        else {
          // do the real turn post, with the turnSequence
          var params = {body:JSON.stringify(turnBody)};
          params = pushParameters(params, parameters);
          var body = undefined;
          RestClient.post("/challenges/instances/"+instanceId+"/turns", params, body, handlers);
        }
      };

      /**
       * Specialized function to submit a challenge player's turn with score only.
       * @param {int} score - raw score
       * @param {object} parameters - (facultative, the score metadata) 'meta':{string}
       * @param {module:Scoreflex.SDK.Handlers} handlers - request callbacks
       *
       * @public
       * @instance
       * @memberof module:Scoreflex.SDK.ChallengeInstance
       */
      var submitTurnScore = function(score, parameters, handlers) {
        var turnBody = {score:score};
        submitTurn(turnBody, parameters, handlers);
      };

      /**
       * Display a web client with the details of a challenge instance.
       * @param {object} parameters - key/value pair of query string parameters
       * @param {object} options - key/value pair of WebClient options
       *
       * @public
       * @instance
       * @memberof module:Scoreflex.SDK.ChallengeInstance
       */
      var showDetails = function(parameters, options) {
        if (!isInitialized()) return;
        var params = pushParameters({}, parameters);
        var defaultOpt = {style:'full'};
        WebClient.show("/web/challenges/instances/"+instanceId, params, options, defaultOpt);
      };

      return {
        getInstanceId:getInstanceId,
        getConfigId:getConfigId,
        getDetails:getDetails,
        getPlayers:getPlayers,
        getTurns:getTurns,
        submitTurn:submitTurn,
        submitTurnScore:submitTurnScore,
        showDetails:showDetails
      };
    };
    //-- ChallengeInstance object end


    //-- Challenges static
    /**
     * Object to get and manipulate challenges.
     * @public
     * @namespace Challenges
     * @memberof module:Scoreflex.SDK
     */
    var Challenges = (function() {
      var cache = {};

      /**
       * Display a web client with the list of challenges of the player.
       * @param {object} parameters - key/value pair of query string parameters
       * @param {object} options - key/value pair of WebClient options
       *
       * @public
       * @memberof module:Scoreflex.SDK.Challenges
       */
      var showChallenges = function(parameters, options) {
        if (!isInitialized()) return;
        var params = pushParameters({}, parameters);
        var defaultOpt = {style:'full'};
        WebClient.show("/web/challenges", params, options, defaultOpt);
      };

      /**
       * Request a challenge and call the onload handler with a
       * {@link module:Scoreflex.SDK.ChallengeInstance} object.
       * @param {string} challengeInstanceId - Challenge instance ID
       * @param {string} challengeConfigId - Challenge configuration ID
       * @return {module:Scoreflex.SDK.ChallengeInstance}
       *
       * @public
       * @memberof module:Scoreflex.SDK.Challenges
       */
      var get = function(challengeInstanceId, challengeConfigId) {
        return ChallengeInstance(challengeInstanceId, challengeConfigId);
      };

      return  {
        showChallenges:showChallenges,
        get:get
      };
    })();
    //-- Challenges static end


    //-- SDK continued
    /**
     * Get Game context.
     * @return {object}
     *
     * @private
     * @memberof module:Scoreflex.SDK
     */
    var getContext = function() {
      return _context;
    };

    /**
     * Get player session.
     * @return {object}
     *
     * @private
     * @memberof module:Scoreflex.SDK
     */
    var getSession = function() {
      var sessions = Storage.get('sessions');
      if (!sessions) sessions = {};
      if (sessions.player) {
        return sessions.player;
      }
      return sessions.anonymous;
    };

    /**
     * Set player session.
     * @param {object} session
     * @param {bool} logged
     *
     * @private
     * @memberof module:Scoreflex.SDK
     */
    var setSession = function(session, logged) {
      var sessions = Storage.get('sessions');
      if (!sessions) sessions = {};
      if (logged === true) {
        sessions.player = session;
      }
      else {
        sessions.anonymous = session;
      }
      Storage.set('sessions', sessions);
    };

    /**
     * Delete player session.
     *
     * @public
     * @memberof module:Scoreflex.SDK
     */
    var reset = function() {
      Storage.rm('sessions');
    };

    /**
     * Check the session is successfully initialized.
     * @return {bool}
     *
     * @private
     * @memberof module:Scoreflex.SDK
     */
    var isInitialized = function() {
      return _initialized;
    };

    /**
     * Get the session initialization state.
     * @return {module:Scoreflex.SessionState}
     *
     * @public
     * @memberof module:Scoreflex.SDK
     */
    var getSessionState = function() {
      return _initState;
    };

    /**
     * Set the session initialization state and send a Session event
     *
     * @private
     * @memberof module:Scoreflex.SDK
     */
    var setSessionState = function(state) {
      _initState = state;
      Events.fire(Events.ScoreflexSessionEvent());
    };


    var _oauthState = null;
    /**
     * Get or generate the oauth state parameter.
     * @param {bool} regen
     * @return {string}
     *
     * @private
     * @memberof module:Scoreflex.SDK
     */
    var getOauthState = function(regen) {
      if (regen === true || _oauthState === null) {
        _oauthState = 'S' + (+(new Date()));
      }
      return _oauthState;
    };

    /**
     * Merge two lists of parameters.
     * @param {object} params
     * @param {object} parameters
     * @return {object} merged list
     *
     * @private
     * @memberof module:Scoreflex.SDK
     */
    var pushParameters = function(params, parameters) {
      if (params === undefined) params = {};
      if (parameters !== undefined) {
        for (var k in parameters) {
          params[k] = parameters[k];
        }
      }
      return params;
    };

    /*++++++++++++++++++++++++
     + + + + API CALLS + + + +
     +++++++++++++++++++++++++*/

    /*
     * == REST API ==
     */

    /**
     * Request an anonymous access token for a guest.
     * @param {module:Scoreflex.SDK.Handlers} handlers
     *
     * @private
     * @memberof module:Scoreflex.SDK
     */
    var fetchAnonymousAccessToken = function(handlers) {
      var context = getContext();
      var params = {
        clientId: context.clientId,
        devicePlatform: 'Web',
        deviceModel: window.navigator.userAgent || 'browser',
        deviceId:SFX.Helper.getDeviceId()
      };
      var body = undefined;
      RestClient.post("/oauth/anonymousAccessToken", params, body, handlers);
    };

    /**
     * Request an anonymous access token for a guest if no local token exists.
     *
     * @private
     * @memberof module:Scoreflex.SDK
     */
    var fetchAnonymousAccessTokenIfNeeded = function(onload, onerror) {
      var session = getSession();

      var onLoad = function() {
        // we may not have a response in case the locally-stored session already existed
        var json = this.responseJSON || {};
        var accessToken = (json.accessToken || {}).token || null;
        var sid = json.sid || null;
        var me = json.me || {};
        var playerId = me.id || null;
        var lang = me.language || null;
        if (accessToken && sid && playerId) {
          setSession({
            anonymous:true,
            accessToken:accessToken,
            sid:sid,
            playerId:playerId,
            lang:lang,
            me:me
          }, false);
        }

        Events.fire(Events.ScoreflexPlayerEvent());

        if (onload) onload();
      };
      var onError = function() {
        if (onerror) onerror();
      };
      var handlers = {
        onload: onLoad,
        onerror: onError
      };

      if (session && session.accessToken && session.sid && session.playerId) {
        if (handlers.onload) {
          setTimeout(handlers.onload, 0);
        }
      }
      else {
        fetchAnonymousAccessToken(handlers);
      }
    };

    /**
     * Request an access token for a logged player.
     * @param {object} data with keys 'code' and 'state'
     *
     * @private
     * @memberof module:Scoreflex.SDK
     */
    var fetchAccessToken = function(data) {
      var state = data.state;
      if (state === getOauthState()) {
        var context = getContext();
        var params = {
          code: data.code,
          clientId: context.clientId
        };
        var body = undefined;

        var onLoad = function() {
          var json = this.responseJSON || {};
          var accessToken = (json.accessToken || {}).token || null;
          var sid = json.sid || null;
          var me = json.me || {};
          var playerId = me.id || null;
          var lang = me.language || null;
          if (accessToken && sid && playerId) {
            // save authenticated player in local session
            setSession({
              anonymous:false,
              accessToken:accessToken,
              sid:sid,
              playerId:playerId,
              lang:lang,
              me:me
            }, true);
            // remove ghost player from local session
            setSession(null, false);
          }
          Players.showProfile();
          Events.fire(Events.ScoreflexPlayerEvent());
        };
        var onError = function() {
          //console.log('error');
        };

        var handlers = {
          onload: onLoad,
          onerror: onError
        };

        RestClient.post("/oauth/accessToken", params, body, handlers);
      }
    };

    /**
     * Ping Scoreflex server.
     * @param {module:Scoreflex.SDK.Handlers} handlers - request callbacks
     *
     * @public
     * @memberof module:Scoreflex.SDK
     */
    var ping = function(handlers) {
      var params = {};
      RestClient.get("/network/ping", params, handlers);
    };

    /**
     * Set environment, ensure we have an accessToken.
     * @param {string} clientId
     * @param {string} clientSecret
     * @param {boolean} useSandbox
     *
     * @private
     * @memberof module:Scoreflex.SDK
     */
    var initialize = function(clientId, clientSecret, useSandbox) {
      if (!isInitialized()) {
        _context.clientId = clientId;
        _context.clientSecret = clientSecret;
        _context.useSandbox = useSandbox;

        setSessionState(Scoreflex.SessionState.INIT_INPROGRESS);
        var onload = function() {
          _initialized = true;
          setSessionState(Scoreflex.SessionState.INIT_SUCCESS);
        };
        var onerror = function() {
          setSessionState(Scoreflex.SessionState.INIT_FAILED);
        };
        fetchAnonymousAccessTokenIfNeeded(onload, onerror);
      }
    };

    /**
     * == WEB API ==
     */

    /**
     * Display web client to signin the current anonymous player.
     * @param {object} parameters
     * @param {object} options
     *
     * @private
     * @memberof module:Scoreflex.SDK
     */
    var authorize = function(parameters, options) {
      if (!isInitialized()) return;
      var session = getSession();
      if (session.anonymous === true) {
        var context = getContext();
        var params = {
          clientId: context.clientId,
          devicePlatform: 'Web',
          deviceModel: window.navigator.userAgent || 'browser',
          deviceId:SFX.Helper.getDeviceId(),
          state: getOauthState(true)
        };
        if (session.accessToken) {
          params.anonymousAccessToken = session.accessToken;
          params.forceMerge = 'true';
        }
        params = pushParameters(params, parameters);
        var defaultOpt = {style:'full', noSid:true};
        WebClient.show("/oauth/web/authorize", params, options, defaultOpt);
      }
    };

    initialize(clientId, clientSecret, useSandbox);

    return {
      // misc
      reset:reset, // delete localStorage session data
      getSessionState:getSessionState,
      // rest api
      RestClient: RestClient,
      ping:ping,
      // web api
      WebClient:WebClient,
      // objects
      Leaderboards: Leaderboards,
      Players: Players,
      Challenges: Challenges
    };

  })(clientId, clientSecret, useSandbox);

  return SFX.SDK;

};


/**
 * List of Session States
 * @readonly
 * @enum {integer}
 * @alias module:Scoreflex.SessionState
 * @memberof module:Scoreflex
 */
Scoreflex.SessionState = {
  /**
   * Session initialization failed
   */
  INIT_FAILED: -1,
  /**
   * Session initialization is not started
   */
  INIT_UNSTARTED: 0,
  /**
   * Session initialization is in progress
   */
  INIT_INPROGRESS: 1,
  /**
   * Session initilialization is successful
   */
  INIT_SUCCESS: 2
};