@hellotext/hellotext 2.3.6 → 2.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/hellotext.js +1 -1
- package/index.d.ts +2 -1
- package/lib/controllers/message_controller.cjs +19 -0
- package/lib/controllers/message_controller.js +19 -0
- package/lib/core/event.cjs +1 -1
- package/lib/core/event.js +1 -1
- package/lib/hellotext.cjs +9 -6
- package/lib/hellotext.js +10 -5
- package/lib/models/fingerprint.cjs +89 -0
- package/lib/models/fingerprint.js +101 -0
- package/lib/models/index.cjs +7 -0
- package/lib/models/index.js +1 -0
- package/lib/models/user.cjs +10 -1
- package/lib/models/user.js +10 -1
- package/package.json +1 -1
- package/src/controllers/message_controller.js +20 -0
- package/src/core/event.js +1 -0
- package/src/hellotext.js +15 -7
- package/src/models/fingerprint.js +98 -0
- package/src/models/index.js +1 -0
- package/src/models/user.js +10 -1
package/index.d.ts
CHANGED
|
@@ -70,7 +70,8 @@ declare class Hellotext {
|
|
|
70
70
|
export declare class User {
|
|
71
71
|
static get id(): string | undefined
|
|
72
72
|
static get source(): string | undefined
|
|
73
|
-
static
|
|
73
|
+
static get fingerprint(): string | undefined
|
|
74
|
+
static remember(id: string, source?: string, fingerprint?: string): void
|
|
74
75
|
static forget(): void
|
|
75
76
|
static get identificationData(): IdentificationData | Record<string, never>
|
|
76
77
|
}
|
|
@@ -5,6 +5,8 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.default = void 0;
|
|
7
7
|
var _stimulus = require("@hotwired/stimulus");
|
|
8
|
+
var _hellotext = _interopRequireDefault(require("../hellotext"));
|
|
9
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
8
10
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
9
11
|
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); } }
|
|
10
12
|
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
|
|
@@ -58,6 +60,23 @@ let _default = /*#__PURE__*/function (_Controller) {
|
|
|
58
60
|
}
|
|
59
61
|
});
|
|
60
62
|
}
|
|
63
|
+
}, {
|
|
64
|
+
key: "addToCart",
|
|
65
|
+
value: function addToCart({
|
|
66
|
+
currentTarget
|
|
67
|
+
}) {
|
|
68
|
+
const card = currentTarget.closest('[data-hellotext--message-target="carouselCard"]');
|
|
69
|
+
const payload = {
|
|
70
|
+
object_parameters: {
|
|
71
|
+
items: [{
|
|
72
|
+
product: card.dataset.id,
|
|
73
|
+
quantity: 1
|
|
74
|
+
}]
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
_hellotext.default.track('cart.added', payload);
|
|
78
|
+
_hellotext.default.eventEmitter.dispatch('cart.added', payload);
|
|
79
|
+
}
|
|
61
80
|
}, {
|
|
62
81
|
key: "moveToLeft",
|
|
63
82
|
value: function moveToLeft() {
|
|
@@ -11,6 +11,7 @@ function _assertThisInitialized(self) { if (self === void 0) { throw new Referen
|
|
|
11
11
|
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
|
|
12
12
|
function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
|
|
13
13
|
import { Controller } from '@hotwired/stimulus';
|
|
14
|
+
import Hellotext from '../hellotext';
|
|
14
15
|
var _default = /*#__PURE__*/function (_Controller) {
|
|
15
16
|
_inherits(_default, _Controller);
|
|
16
17
|
var _super = _createSuper(_default);
|
|
@@ -54,6 +55,24 @@ var _default = /*#__PURE__*/function (_Controller) {
|
|
|
54
55
|
}
|
|
55
56
|
});
|
|
56
57
|
}
|
|
58
|
+
}, {
|
|
59
|
+
key: "addToCart",
|
|
60
|
+
value: function addToCart(_ref3) {
|
|
61
|
+
var {
|
|
62
|
+
currentTarget
|
|
63
|
+
} = _ref3;
|
|
64
|
+
var card = currentTarget.closest('[data-hellotext--message-target="carouselCard"]');
|
|
65
|
+
var payload = {
|
|
66
|
+
object_parameters: {
|
|
67
|
+
items: [{
|
|
68
|
+
product: card.dataset.id,
|
|
69
|
+
quantity: 1
|
|
70
|
+
}]
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
Hellotext.track('cart.added', payload);
|
|
74
|
+
Hellotext.eventEmitter.dispatch('cart.added', payload);
|
|
75
|
+
}
|
|
57
76
|
}, {
|
|
58
77
|
key: "moveToLeft",
|
|
59
78
|
value: function moveToLeft() {
|
package/lib/core/event.cjs
CHANGED
|
@@ -68,4 +68,4 @@ let Event = /*#__PURE__*/function () {
|
|
|
68
68
|
return Event;
|
|
69
69
|
}();
|
|
70
70
|
exports.default = Event;
|
|
71
|
-
Event.events = ['session-set', 'utm-set', 'forms:collected', 'form:completed', 'webchat:mounted', 'webchat:opened', 'webchat:closed', 'webchat:message:sent', 'webchat:message:received'];
|
|
71
|
+
Event.events = ['session-set', 'utm-set', 'forms:collected', 'form:completed', 'webchat:mounted', 'webchat:opened', 'webchat:closed', 'webchat:message:sent', 'webchat:message:received', 'cart.added'];
|
package/lib/core/event.js
CHANGED
|
@@ -63,5 +63,5 @@ var Event = /*#__PURE__*/function () {
|
|
|
63
63
|
}]);
|
|
64
64
|
return Event;
|
|
65
65
|
}();
|
|
66
|
-
Event.events = ['session-set', 'utm-set', 'forms:collected', 'form:completed', 'webchat:mounted', 'webchat:opened', 'webchat:closed', 'webchat:message:sent', 'webchat:message:received'];
|
|
66
|
+
Event.events = ['session-set', 'utm-set', 'forms:collected', 'form:completed', 'webchat:mounted', 'webchat:opened', 'webchat:closed', 'webchat:message:sent', 'webchat:message:received', 'cart.added'];
|
|
67
67
|
export { Event as default };
|
package/lib/hellotext.cjs
CHANGED
|
@@ -85,7 +85,9 @@ let Hellotext = /*#__PURE__*/function () {
|
|
|
85
85
|
* @property { String } [name] - the name of the user
|
|
86
86
|
* @property { String } [source] - the platform specific identifier where this pixel is running on.
|
|
87
87
|
*
|
|
88
|
-
* Identifies a user and attaches the hello_session to the user ID
|
|
88
|
+
* Identifies a user and attaches the hello_session to the user ID.
|
|
89
|
+
* Repeated calls are skipped only when the last successful identify payload
|
|
90
|
+
* for the current session remains unchanged.
|
|
89
91
|
* @param { String } externalId - the user ID
|
|
90
92
|
* @param { IdentificationOptions } options - the options for the identification
|
|
91
93
|
* @returns {Promise<Response>}
|
|
@@ -93,11 +95,12 @@ let Hellotext = /*#__PURE__*/function () {
|
|
|
93
95
|
}, {
|
|
94
96
|
key: "identify",
|
|
95
97
|
value: async function identify(externalId, options = {}) {
|
|
96
|
-
|
|
98
|
+
const fingerprint = await _models.Fingerprint.generate(this.session, externalId, options);
|
|
99
|
+
if (_models.Fingerprint.matches(_models.User.fingerprint, fingerprint)) {
|
|
97
100
|
return new _api.Response(true, {
|
|
98
|
-
json: async () => {
|
|
99
|
-
already_identified: true
|
|
100
|
-
}
|
|
101
|
+
json: async () => ({
|
|
102
|
+
already_identified: true
|
|
103
|
+
})
|
|
101
104
|
});
|
|
102
105
|
}
|
|
103
106
|
const response = await _api.default.identifications.create({
|
|
@@ -105,7 +108,7 @@ let Hellotext = /*#__PURE__*/function () {
|
|
|
105
108
|
...options
|
|
106
109
|
});
|
|
107
110
|
if (response.succeeded) {
|
|
108
|
-
_models.User.remember(externalId, options.source);
|
|
111
|
+
_models.User.remember(externalId, options.source, fingerprint);
|
|
109
112
|
}
|
|
110
113
|
return response;
|
|
111
114
|
}
|
package/lib/hellotext.js
CHANGED
|
@@ -10,7 +10,7 @@ function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typ
|
|
|
10
10
|
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); }
|
|
11
11
|
import { Configuration, Event } from './core';
|
|
12
12
|
import API, { Response } from './api';
|
|
13
|
-
import { Business, FormCollection, Page, Query, Session, User, Webchat } from './models';
|
|
13
|
+
import { Business, Fingerprint, FormCollection, Page, Query, Session, User, Webchat } from './models';
|
|
14
14
|
import { NotInitializedError } from './errors';
|
|
15
15
|
var Hellotext = /*#__PURE__*/function () {
|
|
16
16
|
function Hellotext() {
|
|
@@ -85,7 +85,9 @@ var Hellotext = /*#__PURE__*/function () {
|
|
|
85
85
|
* @property { String } [name] - the name of the user
|
|
86
86
|
* @property { String } [source] - the platform specific identifier where this pixel is running on.
|
|
87
87
|
*
|
|
88
|
-
* Identifies a user and attaches the hello_session to the user ID
|
|
88
|
+
* Identifies a user and attaches the hello_session to the user ID.
|
|
89
|
+
* Repeated calls are skipped only when the last successful identify payload
|
|
90
|
+
* for the current session remains unchanged.
|
|
89
91
|
* @param { String } externalId - the user ID
|
|
90
92
|
* @param { IdentificationOptions } options - the options for the identification
|
|
91
93
|
* @returns {Promise<Response>}
|
|
@@ -95,11 +97,14 @@ var Hellotext = /*#__PURE__*/function () {
|
|
|
95
97
|
value: function () {
|
|
96
98
|
var _identify = _asyncToGenerator(function* (externalId) {
|
|
97
99
|
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
98
|
-
|
|
100
|
+
var fingerprint = yield Fingerprint.generate(this.session, externalId, options);
|
|
101
|
+
if (Fingerprint.matches(User.fingerprint, fingerprint)) {
|
|
99
102
|
return new Response(true, {
|
|
100
103
|
json: function () {
|
|
101
104
|
var _json = _asyncToGenerator(function* () {
|
|
102
|
-
|
|
105
|
+
return {
|
|
106
|
+
already_identified: true
|
|
107
|
+
};
|
|
103
108
|
});
|
|
104
109
|
function json() {
|
|
105
110
|
return _json.apply(this, arguments);
|
|
@@ -112,7 +117,7 @@ var Hellotext = /*#__PURE__*/function () {
|
|
|
112
117
|
user_id: externalId
|
|
113
118
|
}, options));
|
|
114
119
|
if (response.succeeded) {
|
|
115
|
-
User.remember(externalId, options.source);
|
|
120
|
+
User.remember(externalId, options.source, fingerprint);
|
|
116
121
|
}
|
|
117
122
|
return response;
|
|
118
123
|
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.Fingerprint = void 0;
|
|
7
|
+
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
8
|
+
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); } }
|
|
9
|
+
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
|
|
10
|
+
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
|
|
11
|
+
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); }
|
|
12
|
+
function normalizeValue(value) {
|
|
13
|
+
// Collapse "missing" values so callers can add optional fields incrementally
|
|
14
|
+
// without changing the fingerprint when the effective payload is the same.
|
|
15
|
+
if (value === null || value === undefined) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
if (typeof value === 'string') {
|
|
19
|
+
const trimmedValue = value.trim();
|
|
20
|
+
|
|
21
|
+
// Treat blank strings as absent values so "" and an omitted field compare equally.
|
|
22
|
+
return trimmedValue === '' ? undefined : trimmedValue;
|
|
23
|
+
}
|
|
24
|
+
if (Array.isArray(value)) {
|
|
25
|
+
// Preserve array order because, unlike object keys, caller-provided sequence can be meaningful.
|
|
26
|
+
return value.map(item => normalizeValue(item)).filter(item => item !== undefined);
|
|
27
|
+
}
|
|
28
|
+
if (value instanceof Date) {
|
|
29
|
+
// Serialize dates into a stable primitive so equivalent timestamps fingerprint the same way.
|
|
30
|
+
return value.toISOString();
|
|
31
|
+
}
|
|
32
|
+
if (typeof value === 'object') {
|
|
33
|
+
// Canonicalize object shape by sorting keys recursively, so key placement never affects equality.
|
|
34
|
+
const normalizedObject = Object.keys(value).sort((leftKey, rightKey) => leftKey.localeCompare(rightKey)).reduce((result, key) => {
|
|
35
|
+
const normalizedChild = normalizeValue(value[key]);
|
|
36
|
+
if (normalizedChild !== undefined) {
|
|
37
|
+
result[key] = normalizedChild;
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}, {});
|
|
41
|
+
return Object.keys(normalizedObject).length > 0 ? normalizedObject : undefined;
|
|
42
|
+
}
|
|
43
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
function serializePayload(session, userId, options = {}) {
|
|
49
|
+
const normalizedPayload = normalizeValue({
|
|
50
|
+
session,
|
|
51
|
+
user_id: userId,
|
|
52
|
+
...options
|
|
53
|
+
}) || {};
|
|
54
|
+
return JSON.stringify(normalizedPayload);
|
|
55
|
+
}
|
|
56
|
+
function fallbackHash(value) {
|
|
57
|
+
let hash = 5381;
|
|
58
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
59
|
+
hash = hash * 33 ^ value.charCodeAt(index);
|
|
60
|
+
}
|
|
61
|
+
return `v1:${(hash >>> 0).toString(16)}`;
|
|
62
|
+
}
|
|
63
|
+
async function sha256(value) {
|
|
64
|
+
var _globalThis$crypto;
|
|
65
|
+
if (!((_globalThis$crypto = globalThis.crypto) !== null && _globalThis$crypto !== void 0 && _globalThis$crypto.subtle) || typeof TextEncoder === 'undefined') {
|
|
66
|
+
return fallbackHash(value);
|
|
67
|
+
}
|
|
68
|
+
const digest = await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(value));
|
|
69
|
+
const hex = Array.from(new Uint8Array(digest)).map(byte => byte.toString(16).padStart(2, '0')).join('');
|
|
70
|
+
return `v1:${hex}`;
|
|
71
|
+
}
|
|
72
|
+
let Fingerprint = /*#__PURE__*/function () {
|
|
73
|
+
function Fingerprint() {
|
|
74
|
+
_classCallCheck(this, Fingerprint);
|
|
75
|
+
}
|
|
76
|
+
_createClass(Fingerprint, null, [{
|
|
77
|
+
key: "matches",
|
|
78
|
+
value: function matches(storedFingerprint, fingerprint) {
|
|
79
|
+
return !!storedFingerprint && storedFingerprint === fingerprint;
|
|
80
|
+
}
|
|
81
|
+
}, {
|
|
82
|
+
key: "generate",
|
|
83
|
+
value: async function generate(session, userId, options = {}) {
|
|
84
|
+
return await sha256(serializePayload(session, userId, options));
|
|
85
|
+
}
|
|
86
|
+
}]);
|
|
87
|
+
return Fingerprint;
|
|
88
|
+
}();
|
|
89
|
+
exports.Fingerprint = Fingerprint;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
2
|
+
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); } }
|
|
3
|
+
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
|
|
4
|
+
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); } }
|
|
5
|
+
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); }); }; }
|
|
6
|
+
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
|
|
7
|
+
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
|
|
8
|
+
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
|
9
|
+
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
|
|
10
|
+
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); }
|
|
11
|
+
function normalizeValue(value) {
|
|
12
|
+
// Collapse "missing" values so callers can add optional fields incrementally
|
|
13
|
+
// without changing the fingerprint when the effective payload is the same.
|
|
14
|
+
if (value === null || value === undefined) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
if (typeof value === 'string') {
|
|
18
|
+
var trimmedValue = value.trim();
|
|
19
|
+
|
|
20
|
+
// Treat blank strings as absent values so "" and an omitted field compare equally.
|
|
21
|
+
return trimmedValue === '' ? undefined : trimmedValue;
|
|
22
|
+
}
|
|
23
|
+
if (Array.isArray(value)) {
|
|
24
|
+
// Preserve array order because, unlike object keys, caller-provided sequence can be meaningful.
|
|
25
|
+
return value.map(item => normalizeValue(item)).filter(item => item !== undefined);
|
|
26
|
+
}
|
|
27
|
+
if (value instanceof Date) {
|
|
28
|
+
// Serialize dates into a stable primitive so equivalent timestamps fingerprint the same way.
|
|
29
|
+
return value.toISOString();
|
|
30
|
+
}
|
|
31
|
+
if (typeof value === 'object') {
|
|
32
|
+
// Canonicalize object shape by sorting keys recursively, so key placement never affects equality.
|
|
33
|
+
var normalizedObject = Object.keys(value).sort((leftKey, rightKey) => leftKey.localeCompare(rightKey)).reduce((result, key) => {
|
|
34
|
+
var normalizedChild = normalizeValue(value[key]);
|
|
35
|
+
if (normalizedChild !== undefined) {
|
|
36
|
+
result[key] = normalizedChild;
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}, {});
|
|
40
|
+
return Object.keys(normalizedObject).length > 0 ? normalizedObject : undefined;
|
|
41
|
+
}
|
|
42
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
function serializePayload(session, userId) {
|
|
48
|
+
var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
|
|
49
|
+
var normalizedPayload = normalizeValue(_objectSpread({
|
|
50
|
+
session,
|
|
51
|
+
user_id: userId
|
|
52
|
+
}, options)) || {};
|
|
53
|
+
return JSON.stringify(normalizedPayload);
|
|
54
|
+
}
|
|
55
|
+
function fallbackHash(value) {
|
|
56
|
+
var hash = 5381;
|
|
57
|
+
for (var index = 0; index < value.length; index += 1) {
|
|
58
|
+
hash = hash * 33 ^ value.charCodeAt(index);
|
|
59
|
+
}
|
|
60
|
+
return "v1:".concat((hash >>> 0).toString(16));
|
|
61
|
+
}
|
|
62
|
+
function sha256(_x) {
|
|
63
|
+
return _sha.apply(this, arguments);
|
|
64
|
+
}
|
|
65
|
+
function _sha() {
|
|
66
|
+
_sha = _asyncToGenerator(function* (value) {
|
|
67
|
+
var _globalThis$crypto;
|
|
68
|
+
if (!((_globalThis$crypto = globalThis.crypto) !== null && _globalThis$crypto !== void 0 && _globalThis$crypto.subtle) || typeof TextEncoder === 'undefined') {
|
|
69
|
+
return fallbackHash(value);
|
|
70
|
+
}
|
|
71
|
+
var digest = yield globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(value));
|
|
72
|
+
var hex = Array.from(new Uint8Array(digest)).map(byte => byte.toString(16).padStart(2, '0')).join('');
|
|
73
|
+
return "v1:".concat(hex);
|
|
74
|
+
});
|
|
75
|
+
return _sha.apply(this, arguments);
|
|
76
|
+
}
|
|
77
|
+
var Fingerprint = /*#__PURE__*/function () {
|
|
78
|
+
function Fingerprint() {
|
|
79
|
+
_classCallCheck(this, Fingerprint);
|
|
80
|
+
}
|
|
81
|
+
_createClass(Fingerprint, null, [{
|
|
82
|
+
key: "matches",
|
|
83
|
+
value: function matches(storedFingerprint, fingerprint) {
|
|
84
|
+
return !!storedFingerprint && storedFingerprint === fingerprint;
|
|
85
|
+
}
|
|
86
|
+
}, {
|
|
87
|
+
key: "generate",
|
|
88
|
+
value: function () {
|
|
89
|
+
var _generate = _asyncToGenerator(function* (session, userId) {
|
|
90
|
+
var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
|
|
91
|
+
return yield sha256(serializePayload(session, userId, options));
|
|
92
|
+
});
|
|
93
|
+
function generate(_x2, _x3) {
|
|
94
|
+
return _generate.apply(this, arguments);
|
|
95
|
+
}
|
|
96
|
+
return generate;
|
|
97
|
+
}()
|
|
98
|
+
}]);
|
|
99
|
+
return Fingerprint;
|
|
100
|
+
}();
|
|
101
|
+
export { Fingerprint };
|
package/lib/models/index.cjs
CHANGED
|
@@ -15,6 +15,12 @@ Object.defineProperty(exports, "Cookies", {
|
|
|
15
15
|
return _cookies.Cookies;
|
|
16
16
|
}
|
|
17
17
|
});
|
|
18
|
+
Object.defineProperty(exports, "Fingerprint", {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
get: function () {
|
|
21
|
+
return _fingerprint.Fingerprint;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
18
24
|
Object.defineProperty(exports, "Form", {
|
|
19
25
|
enumerable: true,
|
|
20
26
|
get: function () {
|
|
@@ -65,6 +71,7 @@ Object.defineProperty(exports, "Webchat", {
|
|
|
65
71
|
});
|
|
66
72
|
var _business = require("./business");
|
|
67
73
|
var _cookies = require("./cookies");
|
|
74
|
+
var _fingerprint = require("./fingerprint");
|
|
68
75
|
var _form = require("./form");
|
|
69
76
|
var _form_collection = require("./form_collection");
|
|
70
77
|
var _page = require("./page");
|
package/lib/models/index.js
CHANGED
package/lib/models/user.cjs
CHANGED
|
@@ -24,12 +24,20 @@ let User = /*#__PURE__*/function () {
|
|
|
24
24
|
get: function () {
|
|
25
25
|
return _cookies.Cookies.get('hello_user_source');
|
|
26
26
|
}
|
|
27
|
+
}, {
|
|
28
|
+
key: "fingerprint",
|
|
29
|
+
get: function () {
|
|
30
|
+
return _cookies.Cookies.get('hello_user_identification_hash');
|
|
31
|
+
}
|
|
27
32
|
}, {
|
|
28
33
|
key: "remember",
|
|
29
|
-
value: function remember(id, source) {
|
|
34
|
+
value: function remember(id, source, fingerprint) {
|
|
30
35
|
if (source) {
|
|
31
36
|
_cookies.Cookies.set('hello_user_source', source);
|
|
32
37
|
}
|
|
38
|
+
if (fingerprint) {
|
|
39
|
+
_cookies.Cookies.set('hello_user_identification_hash', fingerprint);
|
|
40
|
+
}
|
|
33
41
|
_cookies.Cookies.set('hello_user_id', id);
|
|
34
42
|
}
|
|
35
43
|
}, {
|
|
@@ -37,6 +45,7 @@ let User = /*#__PURE__*/function () {
|
|
|
37
45
|
value: function forget() {
|
|
38
46
|
_cookies.Cookies.delete('hello_user_id');
|
|
39
47
|
_cookies.Cookies.delete('hello_user_source');
|
|
48
|
+
_cookies.Cookies.delete('hello_user_identification_hash');
|
|
40
49
|
}
|
|
41
50
|
}, {
|
|
42
51
|
key: "identificationData",
|
package/lib/models/user.js
CHANGED
|
@@ -18,12 +18,20 @@ var User = /*#__PURE__*/function () {
|
|
|
18
18
|
get: function get() {
|
|
19
19
|
return Cookies.get('hello_user_source');
|
|
20
20
|
}
|
|
21
|
+
}, {
|
|
22
|
+
key: "fingerprint",
|
|
23
|
+
get: function get() {
|
|
24
|
+
return Cookies.get('hello_user_identification_hash');
|
|
25
|
+
}
|
|
21
26
|
}, {
|
|
22
27
|
key: "remember",
|
|
23
|
-
value: function remember(id, source) {
|
|
28
|
+
value: function remember(id, source, fingerprint) {
|
|
24
29
|
if (source) {
|
|
25
30
|
Cookies.set('hello_user_source', source);
|
|
26
31
|
}
|
|
32
|
+
if (fingerprint) {
|
|
33
|
+
Cookies.set('hello_user_identification_hash', fingerprint);
|
|
34
|
+
}
|
|
27
35
|
Cookies.set('hello_user_id', id);
|
|
28
36
|
}
|
|
29
37
|
}, {
|
|
@@ -31,6 +39,7 @@ var User = /*#__PURE__*/function () {
|
|
|
31
39
|
value: function forget() {
|
|
32
40
|
Cookies.delete('hello_user_id');
|
|
33
41
|
Cookies.delete('hello_user_source');
|
|
42
|
+
Cookies.delete('hello_user_identification_hash');
|
|
34
43
|
}
|
|
35
44
|
}, {
|
|
36
45
|
key: "identificationData",
|
package/package.json
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus'
|
|
2
2
|
|
|
3
|
+
import Hellotext from '../hellotext'
|
|
4
|
+
|
|
3
5
|
export default class extends Controller {
|
|
4
6
|
static values = {
|
|
5
7
|
id: String,
|
|
@@ -34,6 +36,24 @@ export default class extends Controller {
|
|
|
34
36
|
})
|
|
35
37
|
}
|
|
36
38
|
|
|
39
|
+
addToCart({ currentTarget }) {
|
|
40
|
+
const card = currentTarget.closest('[data-hellotext--message-target="carouselCard"]')
|
|
41
|
+
|
|
42
|
+
const payload = {
|
|
43
|
+
object_parameters: {
|
|
44
|
+
items: [
|
|
45
|
+
{
|
|
46
|
+
product: card.dataset.id,
|
|
47
|
+
quantity: 1,
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
Hellotext.track('cart.added', payload)
|
|
54
|
+
Hellotext.eventEmitter.dispatch('cart.added', payload)
|
|
55
|
+
}
|
|
56
|
+
|
|
37
57
|
moveToLeft() {
|
|
38
58
|
if (!this.hasCarouselContainerTarget) return
|
|
39
59
|
|
package/src/core/event.js
CHANGED
package/src/hellotext.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Configuration, Event } from './core'
|
|
2
2
|
|
|
3
3
|
import API, { Response } from './api'
|
|
4
|
-
import { Business, FormCollection, Page, Query, Session, User, Webchat } from './models'
|
|
4
|
+
import { Business, Fingerprint, FormCollection, Page, Query, Session, User, Webchat } from './models'
|
|
5
5
|
|
|
6
6
|
import { NotInitializedError } from './errors'
|
|
7
7
|
|
|
@@ -83,17 +83,21 @@ class Hellotext {
|
|
|
83
83
|
* @property { String } [name] - the name of the user
|
|
84
84
|
* @property { String } [source] - the platform specific identifier where this pixel is running on.
|
|
85
85
|
*
|
|
86
|
-
* Identifies a user and attaches the hello_session to the user ID
|
|
86
|
+
* Identifies a user and attaches the hello_session to the user ID.
|
|
87
|
+
* Repeated calls are skipped only when the last successful identify payload
|
|
88
|
+
* for the current session remains unchanged.
|
|
87
89
|
* @param { String } externalId - the user ID
|
|
88
90
|
* @param { IdentificationOptions } options - the options for the identification
|
|
89
91
|
* @returns {Promise<Response>}
|
|
90
92
|
*/
|
|
91
93
|
static async identify(externalId, options = {}) {
|
|
92
|
-
|
|
94
|
+
const fingerprint = await Fingerprint.generate(this.session, externalId, options)
|
|
95
|
+
|
|
96
|
+
if (Fingerprint.matches(User.fingerprint, fingerprint)) {
|
|
93
97
|
return new Response(true, {
|
|
94
|
-
json: async () => {
|
|
95
|
-
already_identified: true
|
|
96
|
-
},
|
|
98
|
+
json: async () => ({
|
|
99
|
+
already_identified: true,
|
|
100
|
+
}),
|
|
97
101
|
})
|
|
98
102
|
}
|
|
99
103
|
|
|
@@ -103,7 +107,11 @@ class Hellotext {
|
|
|
103
107
|
})
|
|
104
108
|
|
|
105
109
|
if (response.succeeded) {
|
|
106
|
-
User.remember(
|
|
110
|
+
User.remember(
|
|
111
|
+
externalId,
|
|
112
|
+
options.source,
|
|
113
|
+
fingerprint,
|
|
114
|
+
)
|
|
107
115
|
}
|
|
108
116
|
|
|
109
117
|
return response
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
function normalizeValue(value) {
|
|
2
|
+
// Collapse "missing" values so callers can add optional fields incrementally
|
|
3
|
+
// without changing the fingerprint when the effective payload is the same.
|
|
4
|
+
if (value === null || value === undefined) {
|
|
5
|
+
return undefined
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (typeof value === 'string') {
|
|
9
|
+
const trimmedValue = value.trim()
|
|
10
|
+
|
|
11
|
+
// Treat blank strings as absent values so "" and an omitted field compare equally.
|
|
12
|
+
return trimmedValue === '' ? undefined : trimmedValue
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (Array.isArray(value)) {
|
|
16
|
+
// Preserve array order because, unlike object keys, caller-provided sequence can be meaningful.
|
|
17
|
+
return value
|
|
18
|
+
.map(item => normalizeValue(item))
|
|
19
|
+
.filter(item => item !== undefined)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (value instanceof Date) {
|
|
23
|
+
// Serialize dates into a stable primitive so equivalent timestamps fingerprint the same way.
|
|
24
|
+
return value.toISOString()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (typeof value === 'object') {
|
|
28
|
+
// Canonicalize object shape by sorting keys recursively, so key placement never affects equality.
|
|
29
|
+
const normalizedObject = Object.keys(value)
|
|
30
|
+
.sort((leftKey, rightKey) => leftKey.localeCompare(rightKey))
|
|
31
|
+
.reduce((result, key) => {
|
|
32
|
+
const normalizedChild = normalizeValue(value[key])
|
|
33
|
+
|
|
34
|
+
if (normalizedChild !== undefined) {
|
|
35
|
+
result[key] = normalizedChild
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return result
|
|
39
|
+
}, {})
|
|
40
|
+
|
|
41
|
+
return Object.keys(normalizedObject).length > 0 ? normalizedObject : undefined
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
45
|
+
return value
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return undefined
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function serializePayload(session, userId, options = {}) {
|
|
52
|
+
const normalizedPayload = normalizeValue({
|
|
53
|
+
session,
|
|
54
|
+
user_id: userId,
|
|
55
|
+
...options,
|
|
56
|
+
}) || {}
|
|
57
|
+
|
|
58
|
+
return JSON.stringify(normalizedPayload)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function fallbackHash(value) {
|
|
62
|
+
let hash = 5381
|
|
63
|
+
|
|
64
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
65
|
+
hash = (hash * 33) ^ value.charCodeAt(index)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return `v1:${(hash >>> 0).toString(16)}`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function sha256(value) {
|
|
72
|
+
if (!globalThis.crypto?.subtle || typeof TextEncoder === 'undefined') {
|
|
73
|
+
return fallbackHash(value)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const digest = await globalThis.crypto.subtle.digest(
|
|
77
|
+
'SHA-256',
|
|
78
|
+
new TextEncoder().encode(value),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
const hex = Array.from(new Uint8Array(digest))
|
|
82
|
+
.map(byte => byte.toString(16).padStart(2, '0'))
|
|
83
|
+
.join('')
|
|
84
|
+
|
|
85
|
+
return `v1:${hex}`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
class Fingerprint {
|
|
89
|
+
static matches(storedFingerprint, fingerprint) {
|
|
90
|
+
return !!storedFingerprint && storedFingerprint === fingerprint
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
static async generate(session, userId, options = {}) {
|
|
94
|
+
return await sha256(serializePayload(session, userId, options))
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export { Fingerprint }
|
package/src/models/index.js
CHANGED