/**
* @file Ajax api managing the chat on the yalst server<br/>
* Created on 22.09.2014 for the yalst-trunk project.
* @copyright (c) 2015 Visisoft OHG. All rights reserved.
* @version 1.0
* @module api
*
* @typedef {Object} Api
* @property {Boolean} isISOCodeInstallation
* @property {String} apiUrl
* @property {Number} fileSizeLimit
* @property {Number} clientCap
* @property {Boolean} isAddressingInformally
* @property {Boolean} isChatConfiguredForEmojis
* @property {Number} noActivityChatPollingInterval
* @property {api.configure} configure - Set up the live support server.
* @property {api.apiAjaxPromise} apiAjaxPromise - basic method to perform the ajax request to the Chat Api
* @property {api.hello} hello - Query the availability of the live support service
* @property {function(Object): Object} mixinPermanentParameters
* @property {function({getParameters:{}, postParameters: {}=, queryFragment: String=}): void} notifyApiAjax - polyfill navigator.sendBeacon analytics data push
* @property {function(String): void} notifyLeave - Analytics data push
* @property {function(String): Promise} leave
* @property {function(): Promise} leaveStartForm
* @property {function(): void} confirmPreChatPresence
* @property {function(Object): String} getCoBrowsingUrl
* @property {function(Number): Promise.<Api.OperatorInfo>} operatorInfo
* @property {function(Object, Object=, String=): Bacon.EventStream} apiAjaxStream
* @property {function(String, String?): String} getContactFormUrlForDepartmentIdAndLanguageNumber
* @description Ajax api managing the chat on the yalst server
*/
define(['Promise', 'lib/functionalContributions', 'dictionary', 'bacon', 'chatButtonParameters',
'lib/jsUtilities', 'ramda'],
/**
* @param Promise
* @param _fn
* @param dict
* @param {Bacon} Bacon
* @param {ChatButtonParameters} chatButtonParameters
* @param _JS
* @returns {{}}
*/
function(Promise, _fn, dict, Bacon, chatButtonParameters, _JS, R)
{
const
spaUrl = new URL(document.URL),
IS_PRESET_BASED_INTEGRATION = /\bpreset_based_integration=(?!0)/.test(spaUrl.search),
IS_EMBEDDED = /\bembedding=(?!0)/.test(spaUrl.search),
CLIENT_CAP = 2.0,
IS_IOS = /iPod|iPad|iPhone.*?OS \d+/.test(navigator.userAgent);
var
siteCode,
apiUrl,
visitorId,
permanentParameters = {
clientcap: CLIENT_CAP,
lang: chatButtonParameters.lang
},
clientConfiguration = {
deviceInfo: "phone",
appCapabilities: []
},
isInvited = _JS.wrapValue(false);
var
rejectUndefineds = reject(compose(eq(undefined), last)),
rejectUndefinedValues = _fn.transformObject(rejectUndefineds);
/** @type Api */
var self = {
apiUrl : '',
fileSizeLimit : 1.0e+6,
isAddressingInformally : false,
isChatConfiguredForEmojis : false,
isISOCodeInstallation : true,
noActivityChatPollingInterval: 5000,
clientCap : CLIENT_CAP
};
self.isISOCodeInstallation = false;
self.mixinPermanentParameters = function(){ throw new Error("mis-scheduled call before configured.");};
/**
* A promise which if resolved indicates that the api is ready to use by third-party custom JavaScript
* @type {Promise}
*/
self.isConfiguredPromise = new Promise(function(resolve)
{
/**
* @typedef {function} api.configure
* @param {Object} options
*/
/**
* Set up the live support server. This method is called before any other methods by the web application
* and will resolve the {@linkcode module:api.isConfiguredPromise}.
* @type {api.configure}
* @param {Object} options
* @param {string} options.siteNumber - The yalst site number (live support access number)
* @param {string} options.apiUrl - The full url of the remote chat api of yalst live support server. E.g. <tt>"https://your-server.de/yalst/chat.api.php"</tt>
* @param {string} [options.visitorId] - The tracking id for the visitor
* @param {Object} options.clientConfiguration - Identifies the device.
* @param {Bacon.Property} options.isInvited - observable of the invitation status
* Possible values: 'phone', 'tablet' or desktop
*/
self.configure = function (options)
{
siteCode = options.siteNumber;
self.apiUrl = apiUrl = options.apiUrl;
visitorId = options.visitorId;
clientConfiguration = mixin(clientConfiguration, options.clientConfiguration);
options.isInvited.onValue(isInvited.set);
permanentParameters.site = options.siteNumber;
self.mixinPermanentParameters = mixin(permanentParameters);
resolve(true);
};
});
self.getContactFormUrlForDepartmentIdAndLanguageNumber = function(departmentId, languageCode)
{
var scriptUrl = join('/', append('contact.php', _fn.initial(split('/', apiUrl))));
var sameSchemeScriptUrl = scriptUrl.replace(/^https?:/, location.protocol);
/// TODO: test mobile.offline.php$site=".$site."&offlineref=prechat/inchat"&dept=departmentId
var urlParameters = {
site: siteCode,
cdept: departmentId,
mobile : true,
preset_based_integration: IS_PRESET_BASED_INTEGRATION ? 1 : 0
};
if (IS_EMBEDDED)
{
urlParameters.embedding = 1;
}
if (languageCode !== undefined)
{
urlParameters.newlang = languageCode;
}
return sameSchemeScriptUrl + "?" + $.param(urlParameters);
};
self.getCoBrowsingUrl = function(otherParameters)
{
var scriptUrl = join('/', append('cobrowse.php', _fn.initial(split('/', apiUrl))));
var sameSchemeScriptUrl = scriptUrl.replace(/^https?:/, location.protocol);
const parameters = mixin(
{
ycobro_site: siteCode,
ycobro_role: 'vis'
},
otherParameters
);
return sameSchemeScriptUrl + "?" + $.param(parameters);
};
function create$apiXhr (getParameters, postParameters, queryFragment, $xhrOptions)
{
var isPostRequest = !isEmpty(keys(postParameters));
var uri = apiUrl;
if ($.param(getParameters))
{
uri += "?";
uri += $.param(getParameters);
if (queryFragment)
{
uri += "&";
uri += queryFragment;
}
}
else if (queryFragment)
{
uri += "?";
uri += queryFragment;
}
return $.ajax(mixin({
dataType: "json",
url: uri ,
type: isPostRequest ? "POST" : "GET",
data: isPostRequest ? postParameters : undefined
}, $xhrOptions || {}));
}
function processApiResult (json)
{
if (json.error)
{
var error = new Error(json.error + ' (#' + json.code + ')');
error.code = json.code;
throw error;
}
return json;
}
function throwAjaxError ($xhr)
{
throw new Error($xhr.statusText + ' (#' + $xhr.status + ')');
}
self.apiAjaxStream = function(getParameters, postParameters, queryFragment)
{
return Bacon.fromBinder(function(sink)
{
var $xhr = create$apiXhr(getParameters, postParameters, queryFragment);
Promise.resolve($xhr)
.then(processApiResult, throwAjaxError)
.then(function(json)
{
sink(new Bacon.Next(json));
},
function(error)
{
/// do not treat cancelling ajax by the program as an error
if (!error.message.match(/^abort/))
{
sink(new Bacon.Error(error.message));
}
}
);
return function cleanUp()
{
if ($xhr.readyState < 4) // before "DONE"
{
$xhr.abort();
}
};
});
};
/**
* @typedef {function} api.apiAjaxPromise
* @param {!Object} getParameters
* @param {Object} [postParameters]
* @param {string} [queryFragment] key value pairs in HTML-form encoded format
* @returns {Promise}
*/
/**
* basic method to perform the ajax request to the Chat Api
* @type {api.apiAjaxPromise}
* @param {!Object} getParameters
* @param {Object} [postParameters]
* @param {string} [queryFragment] key value pairs in HTML-form encoded format
* @param {Object=} $ajaxOptions i.e. {type: 'HEAD'}
* @returns {Promise}
*/
self.apiAjaxPromise = function(getParameters, postParameters, queryFragment, $ajaxOptions)
{
/// dead-lock is possible
return self.isConfiguredPromise.then(function()
{
return Promise.resolve(create$apiXhr(
getParameters,
postParameters,
queryFragment,
$ajaxOptions
))
.then(processApiResult, throwAjaxError);
}
);
};
self.notifyApiAjax = function(options)
{
// exempt Mobile Safari since sendBeacon is buggy there: https://bugs.webkit.org/show_bug.cgi?id=188329
if (navigator.sendBeacon && !IS_IOS)
{
const
theGetParameters = options.getParameters || {},
hasGetParameters = !R.isEmpty(theGetParameters),
// w/o jQuery:
// var searchParams = new URLSearchParams();
// searchParams.append("key", "value");
// searchParams.toString();
requestUrlQueryPart =
(hasGetParameters || options.queryFragment) ?
'?'
+ ( hasGetParameters ? $.param(theGetParameters) : '' )
+ ( (options.queryFragment && hasGetParameters) ? '&' : '' )
+ ( options.queryFragment || '' ) :
"";
navigator.sendBeacon(apiUrl + requestUrlQueryPart, $.param(options.postParameters || {}));
}
else
{
create$apiXhr(
options.getParameters,
options.postParameters,
options.queryFragment,
mixin(
omit(['getParameters', 'postParameters', 'queryFragment', 'onError', 'onSuccess'], options),
{
async: false,
type: "HEAD"
}
)
);
}
};
/**
* @typedef {function} api.hello
* @returns {Promise}
*/
/**
* @typedef {Object} api.helloResponse
* @property {String} salutation
* @property {Number} filesizelimit
* @property {Number} refresh
* @property {Number} charlimit
* @property {Number} refresh
* @property {String} charset
* @property {String} cookiedomain
* @property {String} chat
* @property {Boolean} transcript
* @property {Boolean} rating
* @property {Boolean} contactform
* @property {String} title
* @property {String} welcome
* @property {String} welcome2
* @property {?String} offlinetext
* @property {Boolean} smileys
*/
/**
* Query the availability of the live support service
* @type {api.hello}
* @return {Promise<import("./api").HelloAPIResponse>}
* @example
* // loading the module
* require(['api'], function(serverApi)
* {
* serverApi.isConfiguredPromise.then(function()
* {
* serverApi.hello().then(function(data)
* {
* switch (data.chat)
* {
* case "online":
* console.log("Chat available");
* break;
* case "busy":
* console.log("Agents are busy");
* break;
* case "offline":
* console.log("Try again later");
* break;
* }
* },
* function(error)
* {
* console.error(error.message);
* });
* });
* });
*/
self.hello = function()
{
return self.apiAjaxPromise(self.mixinPermanentParameters(
{
cmd: "hello",
random: Math.floor(Math.random() * 500)
})
).then(
/**
*
* @param {import("./api").HelloAPIResponse} json
* @return {import("./api").HelloAPIResponse}
*/
function(json)
{
self.isISOCodeInstallation = json.charset === "ISO-8859-1";
self.fileSizeLimit = json.filesizelimit;
self.isAddressingInformally = json.salutation === "du";
self.noActivityChatPollingInterval = json.refresh * 1000;
self.isChatConfiguredForEmojis = json.smileys;
return json;
});
};
/**
* Retrieve the welcoming form for a particular department or the
* live support service in general
* @param {string} [department]
* @returns {Promise<import("./api").StartFormAPIResponse>}
*/
self.startForm = function(department)
{
var parameters = self.mixinPermanentParameters({cmd: "startform"});
if (department) {
parameters.dept = department;
}
if (IS_PRESET_BASED_INTEGRATION && globalLiveSupportProviderChatConfiguration.invitationType !== "no-invitation") {
parameters.invite = 1;
}
parameters.preset_choose_dept = globalLiveSupportProviderChatConfiguration.shouldDepartmentBeSelectedByVisitor ? 1 : 0;
return self.apiAjaxPromise(parameters);
};
/**
* @typedef {Object} api.startResponse
* @property {String} session
* @property {Number} chat
*/
/**
* Request a chat with optionally with a department at the
* configured live support service
* @param {Object} options
* @param {string} [options.department] - The service department id.
* @param {string} options.name - The chat name of the visitor.
* @param {string} [options.email] - The visitor's email if given.
* @param {string} [options.query] - The visitor's concern if given.
* @param {string} [options."custom[0]"] - The value of the <tt>custom[0]</tt> field
* in the welcoming form
* @param {string} [options.serializedForm]
* @returns {Promise<api.startResponse>}
*/
self.start = function(options)
{
const
clientString = join(',', concat(clientConfiguration.appCapabilities,
[IS_EMBEDDED ? "in-page" : "full-page", clientConfiguration.deviceInfo])),
chatInitiationTypeParameterByType = {
"auto-invite" : "auto",
"invite": "active",
"load-based-auto-invite": undefined,
"no-invitation": undefined
},
chatInitiationType =
IS_PRESET_BASED_INTEGRATION
? chatInitiationTypeParameterByType[globalLiveSupportProviderChatConfiguration.invitationType]
: isInvited.get() ? (chatButtonParameters.chatType || "active") : undefined,
formSearchParameters = new URLSearchParams(options.serializedForm ? options.serializedForm : "");
var fixedParameters = self.mixinPermanentParameters({
cmd: "start",
client: clientString ,
colors: screen.colorDepth,
resolution : screen.width + "*" + screen.height,
time : (new Date()).toTimeString().match(/^\d\d:\d\d/)[0],
ref: globalLiveSupportProviderChatConfiguration.referrer,
ref2: globalLiveSupportProviderChatConfiguration.referrer2,
/// Allerdings ist im Moment keine Unterscheidung zwischen Auto- und manueller Einladung möglich.
/// Fehlerhafte Parameter sollten durch die Chat-Api berichtigt werden.
chattype: chatInitiationType,
visitorid: globalLiveSupportProviderChatConfiguration.visitorId,
comment: globalLiveSupportProviderChatConfiguration.comment,
});
if (!formSearchParameters.get("name")) {
fixedParameters.name = dict.get("Websurfer");
}
var apiCallParameters = rejectUndefinedValues(
mixin(
fixedParameters,
options.parameters)
);
if (globalLiveSupportProviderChatConfiguration.department)
{
/// 'dept' and 'department' differ in that the former can be hidden in the
/// department selection box
apiCallParameters['dept'] = globalLiveSupportProviderChatConfiguration.department;
}
return self.apiAjaxPromise(apiCallParameters, undefined, options.serializedForm);
};
/**
* Reports the visitor entering a particular page or view
* @param {string} pageId
* @returns {Promise}
*/
self.enter = function(pageId)
{
if (!visitorId)
{
return Promise.resolve();
}
return self.apiAjaxPromise(self.mixinPermanentParameters({
cmd: "enter",
page: pageId,
visitorid: visitorId
}));
};
/**
* Reports the visitor entering the welcoming form page
* @type {function(this:undefined)|*}
* @returns {Promise}
*/
self.enterStartForm = self.enter.bind(undefined, "startform");
/**
* Reports the visitor leaving a particular page or view.<br><br>
* This is may be done by a <em>SJAX (blocking)</em> network request. Therefore it
* is safe to call this method e.g. on <tt>pagehide</tt>
* @param {string} pageId
* @returns {undefined}
*/
self.notifyLeave = function(pageId)
{
if (!visitorId)
{
return;
}
const theGetParameters = {
cmd: "leave",
page: pageId,
visitorid: visitorId
};
self.notifyApiAjax({
getParameters: self.mixinPermanentParameters(theGetParameters)
});
};
/**
* Reports the visitor leaving a particular page or view
* @param {string} pageId
* @returns {Promise}
*/
self.leave = function(pageId)
{
if (!visitorId)
{
return Promise.resolve();
}
return self.apiAjaxPromise(self.mixinPermanentParameters({
cmd: "leave",
page: pageId,
visitorid: visitorId
}));
};
/**
* Reports the visitor leaving the welcoming form page
* @type {function(this:undefined)|*}
* @returns {Promise}
*/
self.leaveStartForm = self.leave.bind(undefined, "startform");
/**
* just touches the visitor's entry in the monitor table.
* No purpose which is not already covered by ENTER and LEAVE commands
*/
self.confirmPreChatPresence = function()
{
if (!visitorId)
{
return;
}
create$apiXhr(
self.mixinPermanentParameters({
cmd: "prechat",
visitorid: visitorId
}),
{},
"",
{ type: 'HEAD' }
);
};
/**
* @typedef {Object} Api.OperatorInfo
* @property {String} picture
*/
/**
*
* @param {Number} operatorId
* @returns {Promise.<Api.OperatorInfo>}
*/
self.operatorInfo = function(operatorId)
{
return self.apiAjaxPromise(self.mixinPermanentParameters({
cmd : "picture",
operatorid: operatorId
}));
};
self.toJSON = function()
{
return {
isISOCodeInstallation: self.isISOCodeInstallation,
fileSizeLimit : self.fileSizeLimit,
siteCode : siteCode,
apiUrl : apiUrl,
visitorId : visitorId
};
};
self.initializeFromJson = function(json)
{
self.isISOCodeInstallation = json.isISOCodeInstallation;
self.fileSizeLimit = json.fileSizeLimit;
siteCode = json.siteCode;
apiUrl = json.apiUrl;
visitorId = json.visitorId;
};
return self;
});