@hellotext/hellotext 2.1.51 → 2.2.1
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/dist/hellotext.js +1 -1
- package/lib/controllers/message_controller.cjs +124 -0
- package/lib/controllers/message_controller.js +120 -0
- package/lib/controllers/webchat_controller.cjs +104 -6
- package/lib/controllers/webchat_controller.js +115 -11
- package/lib/index.cjs +2 -0
- package/lib/index.js +2 -0
- package/lib/models/cookies.cjs +10 -1
- package/lib/models/cookies.js +10 -1
- package/lib/models/page.cjs +79 -1
- package/lib/models/page.js +80 -1
- package/package.json +1 -1
- package/src/controllers/message_controller.js +95 -0
- package/src/controllers/webchat_controller.js +111 -9
- package/src/index.js +2 -0
- package/src/models/cookies.js +10 -1
- package/src/models/page.js +83 -1
package/lib/models/page.cjs
CHANGED
|
@@ -24,7 +24,7 @@ let Page = /*#__PURE__*/function () {
|
|
|
24
24
|
_createClass(Page, [{
|
|
25
25
|
key: "url",
|
|
26
26
|
get: function () {
|
|
27
|
-
return this._url
|
|
27
|
+
return this._url !== null && this._url !== undefined ? this._url : window.location.href;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
@@ -80,6 +80,84 @@ let Page = /*#__PURE__*/function () {
|
|
|
80
80
|
utm_params: this.utmParams
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get the current page domain (root domain for cookie sharing)
|
|
86
|
+
* @returns {string} The root domain with leading dot, or null if no valid URL
|
|
87
|
+
*/
|
|
88
|
+
}, {
|
|
89
|
+
key: "domain",
|
|
90
|
+
get: function () {
|
|
91
|
+
try {
|
|
92
|
+
const url = this.url;
|
|
93
|
+
if (!url) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const hostname = new URL(url).hostname;
|
|
97
|
+
return Page.getRootDomain(hostname);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get the root domain from a hostname (static method, no instance needed)
|
|
105
|
+
* @param {string} hostname - The hostname to parse
|
|
106
|
+
* @returns {string} The root domain with leading dot, or null if invalid
|
|
107
|
+
*/
|
|
108
|
+
}], [{
|
|
109
|
+
key: "getRootDomain",
|
|
110
|
+
value: function getRootDomain(hostname = null) {
|
|
111
|
+
try {
|
|
112
|
+
if (!hostname) {
|
|
113
|
+
var _window$location;
|
|
114
|
+
if (typeof window === 'undefined' || !((_window$location = window.location) !== null && _window$location !== void 0 && _window$location.hostname)) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
hostname = window.location.hostname;
|
|
118
|
+
}
|
|
119
|
+
const parts = hostname.split('.');
|
|
120
|
+
|
|
121
|
+
// Handle localhost or single-part domains
|
|
122
|
+
if (parts.length <= 1) {
|
|
123
|
+
return hostname;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Handle e-commerce platform domains where store identifier must be preserved
|
|
127
|
+
// Examples:
|
|
128
|
+
// storename.myshopify.com -> .storename.myshopify.com
|
|
129
|
+
// storename.vtexcommercestable.com.br -> .storename.vtexcommercestable.com.br
|
|
130
|
+
// storename.wixsite.com -> .storename.wixsite.com
|
|
131
|
+
const platformSuffixes = ['myshopify.com', 'vtexcommercestable.com.br', 'myvtex.com', 'wixsite.com'];
|
|
132
|
+
for (const suffix of platformSuffixes) {
|
|
133
|
+
const suffixParts = suffix.split('.');
|
|
134
|
+
// Get the last N parts of the hostname (e.g., last 2 parts for 'myshopify.com')
|
|
135
|
+
const domainTail = parts.slice(-suffixParts.length).join('.');
|
|
136
|
+
|
|
137
|
+
// Check if the tail exactly matches the platform suffix
|
|
138
|
+
// and there are additional parts for the store identifier
|
|
139
|
+
if (domainTail === suffix && parts.length > suffixParts.length) {
|
|
140
|
+
// Include store identifier + platform suffix
|
|
141
|
+
// e.g., ['secure', 'mystore', 'myshopify', 'com'] -> '.mystore.myshopify.com'
|
|
142
|
+
return `.${parts.slice(-(suffixParts.length + 1)).join('.')}`;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Simple heuristic: if TLD is 2 chars and second-level is short (<=3 chars),
|
|
147
|
+
// it's likely a multi-part TLD like .com.br, .co.uk
|
|
148
|
+
const tld = parts[parts.length - 1];
|
|
149
|
+
const secondLevel = parts[parts.length - 2];
|
|
150
|
+
if (parts.length > 2 && tld.length === 2 && secondLevel.length <= 3) {
|
|
151
|
+
// Multi-part TLD: take last 3 parts (e.g., store.com.br)
|
|
152
|
+
return `.${parts.slice(-3).join('.')}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Regular TLD: take last 2 parts (e.g., example.com)
|
|
156
|
+
return `.${parts.slice(-2).join('.')}`;
|
|
157
|
+
} catch (e) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
83
161
|
}]);
|
|
84
162
|
return Page;
|
|
85
163
|
}();
|
package/lib/models/page.js
CHANGED
|
@@ -19,7 +19,7 @@ var Page = /*#__PURE__*/function () {
|
|
|
19
19
|
_createClass(Page, [{
|
|
20
20
|
key: "url",
|
|
21
21
|
get: function get() {
|
|
22
|
-
return this._url
|
|
22
|
+
return this._url !== null && this._url !== undefined ? this._url : window.location.href;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/**
|
|
@@ -75,6 +75,85 @@ var Page = /*#__PURE__*/function () {
|
|
|
75
75
|
utm_params: this.utmParams
|
|
76
76
|
};
|
|
77
77
|
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get the current page domain (root domain for cookie sharing)
|
|
81
|
+
* @returns {string} The root domain with leading dot, or null if no valid URL
|
|
82
|
+
*/
|
|
83
|
+
}, {
|
|
84
|
+
key: "domain",
|
|
85
|
+
get: function get() {
|
|
86
|
+
try {
|
|
87
|
+
var url = this.url;
|
|
88
|
+
if (!url) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
var hostname = new URL(url).hostname;
|
|
92
|
+
return Page.getRootDomain(hostname);
|
|
93
|
+
} catch (e) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get the root domain from a hostname (static method, no instance needed)
|
|
100
|
+
* @param {string} hostname - The hostname to parse
|
|
101
|
+
* @returns {string} The root domain with leading dot, or null if invalid
|
|
102
|
+
*/
|
|
103
|
+
}], [{
|
|
104
|
+
key: "getRootDomain",
|
|
105
|
+
value: function getRootDomain() {
|
|
106
|
+
var hostname = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
|
|
107
|
+
try {
|
|
108
|
+
if (!hostname) {
|
|
109
|
+
var _window$location;
|
|
110
|
+
if (typeof window === 'undefined' || !((_window$location = window.location) !== null && _window$location !== void 0 && _window$location.hostname)) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
hostname = window.location.hostname;
|
|
114
|
+
}
|
|
115
|
+
var parts = hostname.split('.');
|
|
116
|
+
|
|
117
|
+
// Handle localhost or single-part domains
|
|
118
|
+
if (parts.length <= 1) {
|
|
119
|
+
return hostname;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Handle e-commerce platform domains where store identifier must be preserved
|
|
123
|
+
// Examples:
|
|
124
|
+
// storename.myshopify.com -> .storename.myshopify.com
|
|
125
|
+
// storename.vtexcommercestable.com.br -> .storename.vtexcommercestable.com.br
|
|
126
|
+
// storename.wixsite.com -> .storename.wixsite.com
|
|
127
|
+
var platformSuffixes = ['myshopify.com', 'vtexcommercestable.com.br', 'myvtex.com', 'wixsite.com'];
|
|
128
|
+
for (var suffix of platformSuffixes) {
|
|
129
|
+
var suffixParts = suffix.split('.');
|
|
130
|
+
// Get the last N parts of the hostname (e.g., last 2 parts for 'myshopify.com')
|
|
131
|
+
var domainTail = parts.slice(-suffixParts.length).join('.');
|
|
132
|
+
|
|
133
|
+
// Check if the tail exactly matches the platform suffix
|
|
134
|
+
// and there are additional parts for the store identifier
|
|
135
|
+
if (domainTail === suffix && parts.length > suffixParts.length) {
|
|
136
|
+
// Include store identifier + platform suffix
|
|
137
|
+
// e.g., ['secure', 'mystore', 'myshopify', 'com'] -> '.mystore.myshopify.com'
|
|
138
|
+
return ".".concat(parts.slice(-(suffixParts.length + 1)).join('.'));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Simple heuristic: if TLD is 2 chars and second-level is short (<=3 chars),
|
|
143
|
+
// it's likely a multi-part TLD like .com.br, .co.uk
|
|
144
|
+
var tld = parts[parts.length - 1];
|
|
145
|
+
var secondLevel = parts[parts.length - 2];
|
|
146
|
+
if (parts.length > 2 && tld.length === 2 && secondLevel.length <= 3) {
|
|
147
|
+
// Multi-part TLD: take last 3 parts (e.g., store.com.br)
|
|
148
|
+
return ".".concat(parts.slice(-3).join('.'));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Regular TLD: take last 2 parts (e.g., example.com)
|
|
152
|
+
return ".".concat(parts.slice(-2).join('.'));
|
|
153
|
+
} catch (e) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
78
157
|
}]);
|
|
79
158
|
return Page;
|
|
80
159
|
}();
|
package/package.json
CHANGED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus'
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static values = {
|
|
5
|
+
id: String,
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
static targets = ['carouselContainer', 'leftFade', 'rightFade', 'carouselCard']
|
|
9
|
+
|
|
10
|
+
connect() {
|
|
11
|
+
this.updateFades()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
setId({ detail: id }) {
|
|
15
|
+
this.idValue = id
|
|
16
|
+
this.element.id = id
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
onScroll() {
|
|
20
|
+
this.updateFades()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
quickReply({ currentTarget }) {
|
|
24
|
+
const card = currentTarget.closest('[data-hellotext--message-target="carouselCard"]')
|
|
25
|
+
|
|
26
|
+
this.dispatch('quickReply', {
|
|
27
|
+
detail: {
|
|
28
|
+
id: this.idValue,
|
|
29
|
+
product: card.dataset.id,
|
|
30
|
+
buttonId: currentTarget.dataset.id,
|
|
31
|
+
body: currentTarget.dataset.text,
|
|
32
|
+
cardElement: card,
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
moveToLeft() {
|
|
38
|
+
if (!this.hasCarouselContainerTarget) return
|
|
39
|
+
|
|
40
|
+
const scrollAmount = this.getScrollAmount()
|
|
41
|
+
|
|
42
|
+
this.carouselContainerTarget.scrollBy({
|
|
43
|
+
left: -scrollAmount,
|
|
44
|
+
behavior: 'smooth',
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
moveToRight() {
|
|
49
|
+
if (!this.hasCarouselContainerTarget) return
|
|
50
|
+
|
|
51
|
+
const scrollAmount = this.getScrollAmount()
|
|
52
|
+
|
|
53
|
+
this.carouselContainerTarget.scrollBy({
|
|
54
|
+
left: scrollAmount,
|
|
55
|
+
behavior: 'smooth',
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getScrollAmount() {
|
|
60
|
+
// Get the actual card width from DOM
|
|
61
|
+
const firstCard = this.carouselContainerTarget.querySelector('.message__carousel_card')
|
|
62
|
+
|
|
63
|
+
if (!firstCard) {
|
|
64
|
+
return 280 // Fallback to default desktop card width
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const cardWidth = firstCard.offsetWidth
|
|
68
|
+
const gap = 16 // gap-x-4 = 1rem = 16px
|
|
69
|
+
|
|
70
|
+
return cardWidth + gap
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
updateFades() {
|
|
74
|
+
if (!this.hasCarouselContainerTarget) return
|
|
75
|
+
|
|
76
|
+
const scrollLeft = this.carouselContainerTarget.scrollLeft
|
|
77
|
+
const maxScroll =
|
|
78
|
+
this.carouselContainerTarget.scrollWidth - this.carouselContainerTarget.clientWidth
|
|
79
|
+
|
|
80
|
+
// Show left fade if scrolled past start
|
|
81
|
+
if (scrollLeft > 0) {
|
|
82
|
+
this.leftFadeTarget.classList.remove('hidden')
|
|
83
|
+
} else {
|
|
84
|
+
this.leftFadeTarget.classList.add('hidden')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Show right fade if not at end
|
|
88
|
+
if (scrollLeft < maxScroll - 1) {
|
|
89
|
+
// -1 for rounding errors
|
|
90
|
+
this.rightFadeTarget.classList.remove('hidden')
|
|
91
|
+
} else {
|
|
92
|
+
this.rightFadeTarget.classList.add('hidden')
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -369,6 +369,10 @@ export default class extends Controller {
|
|
|
369
369
|
onMessageReceived(message) {
|
|
370
370
|
const { id, body, attachments } = message
|
|
371
371
|
|
|
372
|
+
if (message.carousel) {
|
|
373
|
+
return this.insertCarouselMessage(message)
|
|
374
|
+
}
|
|
375
|
+
|
|
372
376
|
const div = document.createElement('div')
|
|
373
377
|
div.innerHTML = body
|
|
374
378
|
|
|
@@ -409,6 +413,30 @@ export default class extends Controller {
|
|
|
409
413
|
this.unreadCounterTarget.innerText = unreadCount > 99 ? '99+' : unreadCount
|
|
410
414
|
}
|
|
411
415
|
|
|
416
|
+
insertCarouselMessage(message) {
|
|
417
|
+
const html = message.html
|
|
418
|
+
const element = new DOMParser().parseFromString(html, 'text/html').body.firstElementChild
|
|
419
|
+
|
|
420
|
+
this.clearTypingIndicator()
|
|
421
|
+
this.messagesContainerTarget.appendChild(element)
|
|
422
|
+
|
|
423
|
+
element.scrollIntoView({ behavior: 'smooth' })
|
|
424
|
+
|
|
425
|
+
Hellotext.eventEmitter.dispatch('webchat:message:received', {
|
|
426
|
+
...message,
|
|
427
|
+
body: element.querySelector('[data-body]')?.innerText || '',
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
if (this.openValue) {
|
|
431
|
+
this.messagesAPI.markAsSeen(message.id)
|
|
432
|
+
return
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
this.unreadCounterTarget.style.display = 'flex'
|
|
436
|
+
const unreadCount = (parseInt(this.unreadCounterTarget.innerText) || 0) + 1
|
|
437
|
+
this.unreadCounterTarget.innerText = unreadCount > 99 ? '99+' : unreadCount
|
|
438
|
+
}
|
|
439
|
+
|
|
412
440
|
resizeInput() {
|
|
413
441
|
const maxHeight = 96
|
|
414
442
|
|
|
@@ -422,6 +450,73 @@ export default class extends Controller {
|
|
|
422
450
|
this.inputTarget.style.height = `${Math.min(scrollHeight, maxHeight)}px`
|
|
423
451
|
}
|
|
424
452
|
|
|
453
|
+
async sendQuickReplyMessage({ detail: { id, product, buttonId, body, cardElement } }) {
|
|
454
|
+
const formData = new FormData()
|
|
455
|
+
|
|
456
|
+
formData.append('message[body]', body)
|
|
457
|
+
formData.append('message[replied_to]', id)
|
|
458
|
+
formData.append('message[product]', product)
|
|
459
|
+
formData.append('message[button]', buttonId)
|
|
460
|
+
|
|
461
|
+
formData.append('session', Hellotext.session)
|
|
462
|
+
formData.append('locale', Locale.toString())
|
|
463
|
+
|
|
464
|
+
const element = this.buildMessageElement()
|
|
465
|
+
const attachment = cardElement.querySelector('img')?.cloneNode(true)
|
|
466
|
+
|
|
467
|
+
element.querySelector('[data-body]').innerText = body
|
|
468
|
+
|
|
469
|
+
if (attachment) {
|
|
470
|
+
attachment.removeAttribute('width')
|
|
471
|
+
attachment.removeAttribute('height')
|
|
472
|
+
|
|
473
|
+
element.querySelector('[data-attachment-container]').appendChild(attachment)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (this.typingIndicatorVisible && this.hasTypingIndicatorTarget) {
|
|
477
|
+
this.messagesContainerTarget.insertBefore(element, this.typingIndicatorTarget)
|
|
478
|
+
} else {
|
|
479
|
+
this.messagesContainerTarget.appendChild(element)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
element.scrollIntoView({ behavior: 'smooth' })
|
|
483
|
+
|
|
484
|
+
this.broadcastChannel.postMessage({
|
|
485
|
+
type: 'message:sent',
|
|
486
|
+
element: element.outerHTML,
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
const response = await this.messagesAPI.create(formData)
|
|
490
|
+
|
|
491
|
+
if (response.failed) {
|
|
492
|
+
// Clear the optimistic typing indicator on failure
|
|
493
|
+
clearTimeout(this.optimisticTypingTimeout)
|
|
494
|
+
|
|
495
|
+
this.broadcastChannel.postMessage({
|
|
496
|
+
type: 'message:failed',
|
|
497
|
+
id: element.id,
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
return element.classList.add('failed')
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const data = await response.json()
|
|
504
|
+
|
|
505
|
+
this.dispatch('set:id', { target: element, detail: data.id })
|
|
506
|
+
|
|
507
|
+
const message = {
|
|
508
|
+
id: data.id,
|
|
509
|
+
body: body,
|
|
510
|
+
attachments: attachment ? [attachment.src] : [],
|
|
511
|
+
replied_to: id,
|
|
512
|
+
product: product,
|
|
513
|
+
button: buttonId,
|
|
514
|
+
type: 'quick_reply',
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
Hellotext.eventEmitter.dispatch('webchat:message:sent', message)
|
|
518
|
+
}
|
|
519
|
+
|
|
425
520
|
async sendMessage(e) {
|
|
426
521
|
const formData = new FormData()
|
|
427
522
|
|
|
@@ -451,14 +546,7 @@ export default class extends Controller {
|
|
|
451
546
|
formData.append('session', Hellotext.session)
|
|
452
547
|
formData.append('locale', Locale.toString())
|
|
453
548
|
|
|
454
|
-
const element = this.
|
|
455
|
-
|
|
456
|
-
element.id = `hellotext--webchat--${this.idValue}--message--${Date.now()}`
|
|
457
|
-
|
|
458
|
-
element.classList.add('received')
|
|
459
|
-
element.style.removeProperty('display')
|
|
460
|
-
|
|
461
|
-
element.setAttribute('data-hellotext--webchat-target', 'message')
|
|
549
|
+
const element = this.buildMessageElement()
|
|
462
550
|
|
|
463
551
|
if (this.inputTarget.value.trim().length > 0) {
|
|
464
552
|
element.querySelector('[data-body]').innerText = this.inputTarget.value
|
|
@@ -470,7 +558,7 @@ export default class extends Controller {
|
|
|
470
558
|
|
|
471
559
|
if (attachments.length > 0) {
|
|
472
560
|
attachments.forEach(attachment => {
|
|
473
|
-
element.querySelector('[data-attachment-container]').appendChild(attachment)
|
|
561
|
+
element.querySelector('[data-attachment-container]').appendChild(attachment.cloneNode(true))
|
|
474
562
|
})
|
|
475
563
|
}
|
|
476
564
|
|
|
@@ -542,6 +630,20 @@ export default class extends Controller {
|
|
|
542
630
|
this.attachmentContainerTarget.style.display = ''
|
|
543
631
|
}
|
|
544
632
|
|
|
633
|
+
buildMessageElement() {
|
|
634
|
+
const element = this.messageTemplateTarget.cloneNode(true)
|
|
635
|
+
|
|
636
|
+
element.id = `hellotext--webchat--${this.idValue}--message--${Date.now()}`
|
|
637
|
+
|
|
638
|
+
element.classList.add('received')
|
|
639
|
+
element.style.removeProperty('display')
|
|
640
|
+
|
|
641
|
+
element.setAttribute('data-controller', 'hellotext--message')
|
|
642
|
+
element.setAttribute('data-hellotext--webchat-target', 'message')
|
|
643
|
+
|
|
644
|
+
return element
|
|
645
|
+
}
|
|
646
|
+
|
|
545
647
|
openAttachment() {
|
|
546
648
|
this.attachmentInputTarget.click()
|
|
547
649
|
}
|
package/src/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Application } from '@hotwired/stimulus'
|
|
|
2
2
|
import Hellotext from './hellotext'
|
|
3
3
|
|
|
4
4
|
import FormController from './controllers/form_controller'
|
|
5
|
+
import MessageController from './controllers/message_controller'
|
|
5
6
|
import WebChatEmojiController from './controllers/webchat/emoji_picker_controller'
|
|
6
7
|
import WebchatController from './controllers/webchat_controller'
|
|
7
8
|
|
|
@@ -10,6 +11,7 @@ const application = Application.start()
|
|
|
10
11
|
application.register('hellotext--form', FormController)
|
|
11
12
|
application.register('hellotext--webchat', WebchatController)
|
|
12
13
|
application.register('hellotext--webchat--emoji', WebChatEmojiController)
|
|
14
|
+
application.register('hellotext--message', MessageController)
|
|
13
15
|
|
|
14
16
|
window.Hellotext = Hellotext
|
|
15
17
|
|
package/src/models/cookies.js
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import Hellotext from '../hellotext'
|
|
2
|
+
import { Page } from './page'
|
|
2
3
|
|
|
3
4
|
class Cookies {
|
|
4
5
|
static set(name, value) {
|
|
5
6
|
if (typeof document !== 'undefined') {
|
|
6
|
-
|
|
7
|
+
const secure = window.location.protocol === 'https:' ? '; Secure' : ''
|
|
8
|
+
const domain = Page.getRootDomain()
|
|
9
|
+
const maxAge = 10 * 365 * 24 * 60 * 60 // 10 years in seconds
|
|
10
|
+
|
|
11
|
+
if (domain) {
|
|
12
|
+
document.cookie = `${name}=${value}; path=/${secure}; domain=${domain}; max-age=${maxAge}; SameSite=Lax`
|
|
13
|
+
} else {
|
|
14
|
+
document.cookie = `${name}=${value}; path=/${secure}; max-age=${maxAge}; SameSite=Lax`
|
|
15
|
+
}
|
|
7
16
|
}
|
|
8
17
|
|
|
9
18
|
if (name === 'hello_session') {
|
package/src/models/page.js
CHANGED
|
@@ -11,7 +11,7 @@ class Page {
|
|
|
11
11
|
* @returns {string} The page URL
|
|
12
12
|
*/
|
|
13
13
|
get url() {
|
|
14
|
-
return this._url
|
|
14
|
+
return this._url !== null && this._url !== undefined ? this._url : window.location.href
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
@@ -60,6 +60,88 @@ class Page {
|
|
|
60
60
|
utm_params: this.utmParams,
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get the current page domain (root domain for cookie sharing)
|
|
66
|
+
* @returns {string} The root domain with leading dot, or null if no valid URL
|
|
67
|
+
*/
|
|
68
|
+
get domain() {
|
|
69
|
+
try {
|
|
70
|
+
const url = this.url
|
|
71
|
+
if (!url) {
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const hostname = new URL(url).hostname
|
|
76
|
+
return Page.getRootDomain(hostname)
|
|
77
|
+
} catch (e) {
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get the root domain from a hostname (static method, no instance needed)
|
|
84
|
+
* @param {string} hostname - The hostname to parse
|
|
85
|
+
* @returns {string} The root domain with leading dot, or null if invalid
|
|
86
|
+
*/
|
|
87
|
+
static getRootDomain(hostname = null) {
|
|
88
|
+
try {
|
|
89
|
+
if (!hostname) {
|
|
90
|
+
if (typeof window === 'undefined' || !window.location?.hostname) {
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
hostname = window.location.hostname
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const parts = hostname.split('.')
|
|
97
|
+
|
|
98
|
+
// Handle localhost or single-part domains
|
|
99
|
+
if (parts.length <= 1) {
|
|
100
|
+
return hostname
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Handle e-commerce platform domains where store identifier must be preserved
|
|
104
|
+
// Examples:
|
|
105
|
+
// storename.myshopify.com -> .storename.myshopify.com
|
|
106
|
+
// storename.vtexcommercestable.com.br -> .storename.vtexcommercestable.com.br
|
|
107
|
+
// storename.wixsite.com -> .storename.wixsite.com
|
|
108
|
+
const platformSuffixes = [
|
|
109
|
+
'myshopify.com',
|
|
110
|
+
'vtexcommercestable.com.br',
|
|
111
|
+
'myvtex.com',
|
|
112
|
+
'wixsite.com',
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
for (const suffix of platformSuffixes) {
|
|
116
|
+
const suffixParts = suffix.split('.')
|
|
117
|
+
// Get the last N parts of the hostname (e.g., last 2 parts for 'myshopify.com')
|
|
118
|
+
const domainTail = parts.slice(-suffixParts.length).join('.')
|
|
119
|
+
|
|
120
|
+
// Check if the tail exactly matches the platform suffix
|
|
121
|
+
// and there are additional parts for the store identifier
|
|
122
|
+
if (domainTail === suffix && parts.length > suffixParts.length) {
|
|
123
|
+
// Include store identifier + platform suffix
|
|
124
|
+
// e.g., ['secure', 'mystore', 'myshopify', 'com'] -> '.mystore.myshopify.com'
|
|
125
|
+
return `.${parts.slice(-(suffixParts.length + 1)).join('.')}`
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Simple heuristic: if TLD is 2 chars and second-level is short (<=3 chars),
|
|
130
|
+
// it's likely a multi-part TLD like .com.br, .co.uk
|
|
131
|
+
const tld = parts[parts.length - 1]
|
|
132
|
+
const secondLevel = parts[parts.length - 2]
|
|
133
|
+
|
|
134
|
+
if (parts.length > 2 && tld.length === 2 && secondLevel.length <= 3) {
|
|
135
|
+
// Multi-part TLD: take last 3 parts (e.g., store.com.br)
|
|
136
|
+
return `.${parts.slice(-3).join('.')}`
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Regular TLD: take last 2 parts (e.g., example.com)
|
|
140
|
+
return `.${parts.slice(-2).join('.')}`
|
|
141
|
+
} catch (e) {
|
|
142
|
+
return null
|
|
143
|
+
}
|
|
144
|
+
}
|
|
63
145
|
}
|
|
64
146
|
|
|
65
147
|
export { Page }
|