Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions __tests__/models/session_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,21 @@ jest.mock('../../src/hellotext', () => {
}
})

jest.mock('../../src/api', () => {
return {
__esModule: true,
default: {
acks: {
send: jest.fn()
}
}
}
})

import { Configuration } from '../../src/core'
import { Session } from '../../src/models'
import { Cookies } from '../../src/models/cookies'
import API from '../../src/api'

describe('Session', () => {
beforeEach(() => {
Expand All @@ -38,6 +50,7 @@ describe('Session', () => {
}

global.crypto.randomUUID.mockClear()
API.acks.send.mockClear()

if (typeof window !== 'undefined') {
delete window.location
Expand Down Expand Up @@ -382,6 +395,79 @@ describe('Session', () => {
})
})

describe('ack behavior', () => {
it('sends an ack when a session is set for the first time', () => {
Session.session = 'new-session'

expect(API.acks.send).toHaveBeenCalledTimes(1)
})

it('sets hello_session_ack_at cookie after sending an ack', () => {
Session.session = 'new-session'

expect(Cookies.get('hello_session_ack_at')).toBeTruthy()
})

it('does not send an ack when the same session is set again and already acked', () => {
Cookies.set('hello_session', 'existing-session')
Cookies.set('hello_session_ack_at', new Date().toISOString())

Session.session = 'existing-session'

expect(API.acks.send).not.toHaveBeenCalled()
})

it('does not delete hello_session_ack_at when the same session is set again', () => {
const ackedAt = new Date().toISOString()
Cookies.set('hello_session', 'existing-session')
Cookies.set('hello_session_ack_at', ackedAt)

Session.session = 'existing-session'

expect(Cookies.get('hello_session_ack_at')).toEqual(ackedAt)
})

it('sends an ack when the session changes (identity change)', () => {
Cookies.set('hello_session', 'old-session')
Cookies.set('hello_session_ack_at', new Date().toISOString())

Session.session = 'new-session'

expect(API.acks.send).toHaveBeenCalledTimes(1)
})

it('deletes hello_session_ack_at when the session changes', () => {
Cookies.set('hello_session', 'old-session')
Cookies.set('hello_session_ack_at', new Date().toISOString())

Session.session = 'new-session'

// ack_at is deleted and re-set after the new ack
expect(Cookies.get('hello_session_ack_at')).toBeTruthy()
expect(API.acks.send).toHaveBeenCalledTimes(1)
})

it('re-acks each time the session changes to a different value', () => {
Session.session = 'session-a'
expect(API.acks.send).toHaveBeenCalledTimes(1)

Session.session = 'session-b'
expect(API.acks.send).toHaveBeenCalledTimes(2)

Session.session = 'session-c'
expect(API.acks.send).toHaveBeenCalledTimes(3)
})

it('does not re-ack when the same session is set multiple times', () => {
Session.session = 'session-a'
expect(API.acks.send).toHaveBeenCalledTimes(1)

Session.session = 'session-a'
Session.session = 'session-a'
expect(API.acks.send).toHaveBeenCalledTimes(1)
})
})

describe('error handling', () => {
it('handles crypto.randomUUID throwing an error', () => {
Configuration.session = null
Expand Down
2 changes: 1 addition & 1 deletion dist/hellotext.js

Large diffs are not rendered by default.

42 changes: 42 additions & 0 deletions lib/api/acks.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"use strict";

Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _hellotext = _interopRequireDefault(require("../hellotext"));
var _core = require("../core");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
let AcksAPI = /*#__PURE__*/function () {
function AcksAPI() {
_classCallCheck(this, AcksAPI);
}
_createClass(AcksAPI, null, [{
key: "endpoint",
get: function () {
return _core.Configuration.endpoint('public/acks');
}
}, {
key: "send",
value: async function send() {
const payload = {
session: _hellotext.default.session,
at: new Date().toISOString()
};
fetch(this.endpoint, {
method: 'POST',
headers: _hellotext.default.headers,
body: JSON.stringify(payload),
keepalive: true
});
}
}]);
return AcksAPI;
}();
var _default = AcksAPI;
exports.default = _default;
42 changes: 42 additions & 0 deletions lib/api/acks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
import Hellotext from '../hellotext';
import { Configuration } from '../core';
var AcksAPI = /*#__PURE__*/function () {
function AcksAPI() {
_classCallCheck(this, AcksAPI);
}
_createClass(AcksAPI, null, [{
key: "endpoint",
get: function get() {
return Configuration.endpoint('public/acks');
}
}, {
key: "send",
value: function () {
var _send = _asyncToGenerator(function* () {
var payload = {
session: Hellotext.session,
at: new Date().toISOString()
};
fetch(this.endpoint, {
method: 'POST',
headers: Hellotext.headers,
body: JSON.stringify(payload),
keepalive: true
});
});
function send() {
return _send.apply(this, arguments);
}
return send;
}()
}]);
return AcksAPI;
}();
export default AcksAPI;
6 changes: 6 additions & 0 deletions lib/api/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ var _events = _interopRequireDefault(require("./events"));
var _forms = _interopRequireDefault(require("./forms"));
var _identifications = _interopRequireDefault(require("./identifications"));
var _webchats = _interopRequireDefault(require("./webchats"));
var _acks = _interopRequireDefault(require("./acks"));
var _response = require("./response");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
Expand Down Expand Up @@ -51,6 +52,11 @@ let API = /*#__PURE__*/function () {
get: function () {
return _identifications.default;
}
}, {
key: "acks",
get: function () {
return _acks.default;
}
}]);
return API;
}();
Expand Down
6 changes: 6 additions & 0 deletions lib/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import EventsAPI from './events';
import FormsAPI from './forms';
import IdentificationsAPI from './identifications';
import WebchatsAPI from './webchats';
import AcksAPI from './acks';
var API = /*#__PURE__*/function () {
function API() {
_classCallCheck(this, API);
Expand Down Expand Up @@ -37,6 +38,11 @@ var API = /*#__PURE__*/function () {
get: function get() {
return IdentificationsAPI;
}
}, {
key: "acks",
get: function get() {
return AcksAPI;
}
}]);
return API;
}();
Expand Down
4 changes: 2 additions & 2 deletions lib/hellotext.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ let Hellotext = /*#__PURE__*/function () {
* @param { Configuration } config
*/
async function initialize(business, config) {
this.business = new _models.Business(business);
_core.Configuration.assign(config);
_models.Session.initialize();
this.page = new _models.Page();
this.business = new _models.Business(business);
this.forms = new _models.FormCollection();
this.query = new _models.Query();
if (_core.Configuration.webchat.id) {
Expand Down Expand Up @@ -167,7 +167,7 @@ let Hellotext = /*#__PURE__*/function () {
}, {
key: "notInitialized",
get: function () {
return this.business.id === undefined;
return !this.business || this.business.id === undefined;
}
}, {
key: "headers",
Expand Down
4 changes: 2 additions & 2 deletions lib/hellotext.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ var Hellotext = /*#__PURE__*/function () {
*/
function () {
var _initialize = _asyncToGenerator(function* (business, config) {
this.business = new Business(business);
Configuration.assign(config);
Session.initialize();
this.page = new Page();
this.business = new Business(business);
this.forms = new FormCollection();
this.query = new Query();
if (Configuration.webchat.id) {
Expand Down Expand Up @@ -178,7 +178,7 @@ var Hellotext = /*#__PURE__*/function () {
}, {
key: "notInitialized",
get: function get() {
return this.business.id === undefined;
return !this.business || this.business.id === undefined;
}
}, {
key: "headers",
Expand Down
11 changes: 11 additions & 0 deletions lib/models/session.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ exports.Session = void 0;
var _core = require("../core");
var _cookies = require("./cookies");
var _query2 = require("./query");
var _api = _interopRequireDefault(require("../api"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
Expand All @@ -27,8 +29,17 @@ let Session = /*#__PURE__*/function () {
return _classPrivateFieldLooseBase(this, _session)[_session];
},
set: function (value) {
const oldSession = _cookies.Cookies.get('hello_session');
_classPrivateFieldLooseBase(this, _session)[_session] = value;
_cookies.Cookies.set('hello_session', value);
if (oldSession !== value) {
_cookies.Cookies.delete('hello_session_ack_at');
}
if (!_cookies.Cookies.get('hello_session_ack_at')) {
_api.default.acks.send();
_cookies.Cookies.set('hello_session_ack_at', new Date().toISOString());
}
return _classPrivateFieldLooseBase(this, _session)[_session];
}
}, {
key: "initialize",
Expand Down
10 changes: 10 additions & 0 deletions lib/models/session.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ function _classPrivateFieldLooseKey(name) { return "__private_" + id++ + "_" + n
import { Configuration } from '../core';
import { Cookies } from './cookies';
import { Query } from './query';
import API from '../api';
var _session = /*#__PURE__*/_classPrivateFieldLooseKey("session");
var _query = /*#__PURE__*/_classPrivateFieldLooseKey("query");
var Session = /*#__PURE__*/function () {
Expand All @@ -21,8 +22,17 @@ var Session = /*#__PURE__*/function () {
return _classPrivateFieldLooseBase(this, _session)[_session];
},
set: function set(value) {
var oldSession = Cookies.get('hello_session');
_classPrivateFieldLooseBase(this, _session)[_session] = value;
Cookies.set('hello_session', value);
if (oldSession !== value) {
Cookies.delete('hello_session_ack_at');
}
if (!Cookies.get('hello_session_ack_at')) {
API.acks.send();
Cookies.set('hello_session_ack_at', new Date().toISOString());
}
return _classPrivateFieldLooseBase(this, _session)[_session];
}
}, {
key: "initialize",
Expand Down
25 changes: 25 additions & 0 deletions src/api/acks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Hellotext from '../hellotext'

import { Configuration } from '../core'

class AcksAPI {
static get endpoint() {
return Configuration.endpoint('public/acks')
}

static async send() {
const payload = {
session: Hellotext.session,
at: new Date().toISOString(),
}

fetch(this.endpoint, {
method: 'POST',
headers: Hellotext.headers,
body: JSON.stringify(payload),
keepalive: true,
})
}
}

export default AcksAPI
5 changes: 5 additions & 0 deletions src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import EventsAPI from './events'
import FormsAPI from './forms'
import IdentificationsAPI from './identifications'
import WebchatsAPI from './webchats'
import AcksAPI from './acks'

export default class API {
static get businesses() {
Expand All @@ -24,6 +25,10 @@ export default class API {
static get identifications() {
return IdentificationsAPI
}

static get acks() {
return AcksAPI
}
}

export { Response } from './response'
Loading