@product7/product7-js 0.6.5 → 0.6.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.
@@ -4282,10 +4282,173 @@
4282
4282
  opacity: 0.7;
4283
4283
  cursor: not-allowed;
4284
4284
  }
4285
+
4286
+ /* ========================================
4287
+ FEEDBACK FORM VIEW
4288
+ ======================================== */
4289
+
4290
+ .liveChat-feedback-view {
4291
+ display: flex;
4292
+ flex-direction: column;
4293
+ height: 100%;
4294
+ overflow: hidden;
4295
+ }
4296
+
4297
+ .liveChat-feedback-header {
4298
+ display: flex;
4299
+ align-items: center;
4300
+ gap: var(--spacing-3);
4301
+ padding: var(--spacing-4) var(--spacing-4);
4302
+ border-bottom: 1px solid var(--border-color);
4303
+ flex-shrink: 0;
4304
+ }
4305
+
4306
+ .liveChat-feedback-title {
4307
+ font-size: var(--font-size-base);
4308
+ font-weight: var(--font-weight-semibold);
4309
+ color: var(--text-primary);
4310
+ }
4311
+
4312
+ .liveChat-feedback-body {
4313
+ display: flex;
4314
+ flex-direction: column;
4315
+ gap: var(--spacing-4);
4316
+ padding: var(--spacing-5) var(--spacing-4);
4317
+ flex: 1;
4318
+ overflow-y: auto;
4319
+ }
4320
+
4321
+ .liveChat-feedback-prompt {
4322
+ margin: 0;
4323
+ font-size: var(--font-size-sm);
4324
+ color: var(--text-secondary);
4325
+ }
4326
+
4327
+
4328
+ .liveChat-feedback-input {
4329
+ width: 100%;
4330
+ border: 1px solid var(--border-color);
4331
+ border-radius: var(--radius-md);
4332
+ padding: var(--spacing-3);
4333
+ font-size: var(--font-size-sm);
4334
+ font-family: inherit;
4335
+ color: var(--text-primary);
4336
+ background: var(--msg-bg);
4337
+ box-sizing: border-box;
4338
+ transition: border-color var(--transition-fast);
4339
+ }
4340
+
4341
+ .liveChat-feedback-input::placeholder {
4342
+ color: var(--msg-text-secondary);
4343
+ }
4344
+
4345
+ .liveChat-feedback-input:focus {
4346
+ outline: none;
4347
+ border-color: var(--color-primary);
4348
+ }
4349
+
4350
+ .liveChat-feedback-textarea {
4351
+ width: 100%;
4352
+ resize: none;
4353
+ border: 1px solid var(--border-color);
4354
+ border-radius: var(--radius-md);
4355
+ padding: var(--spacing-3);
4356
+ font-size: var(--font-size-sm);
4357
+ font-family: inherit;
4358
+ color: var(--text-primary);
4359
+ background: var(--msg-bg);
4360
+ box-sizing: border-box;
4361
+ transition: border-color var(--transition-fast);
4362
+ }
4363
+
4364
+ .liveChat-feedback-textarea::placeholder {
4365
+ color: var(--msg-text-secondary);
4366
+ }
4367
+
4368
+ .liveChat-feedback-textarea:focus {
4369
+ outline: none;
4370
+ border-color: var(--color-primary);
4371
+ }
4372
+
4373
+ .liveChat-feedback-submit {
4374
+ display: flex;
4375
+ align-items: center;
4376
+ justify-content: center;
4377
+ padding: var(--spacing-3);
4378
+ border-radius: var(--radius-md);
4379
+ font-size: var(--font-size-sm);
4380
+ font-weight: var(--font-weight-medium);
4381
+ font-family: inherit;
4382
+ cursor: pointer;
4383
+ border: none;
4384
+ background: var(--color-primary);
4385
+ color: #ffffff;
4386
+ transition: all var(--transition-fast);
4387
+ }
4388
+
4389
+ .liveChat-feedback-submit:hover:not(:disabled) {
4390
+ background: var(--color-primary-hover);
4391
+ }
4392
+
4393
+ .liveChat-feedback-submit:disabled {
4394
+ opacity: 0.5;
4395
+ cursor: not-allowed;
4396
+ }
4397
+
4398
+ .liveChat-feedback-thankyou {
4399
+ display: flex;
4400
+ flex-direction: column;
4401
+ align-items: center;
4402
+ justify-content: center;
4403
+ flex: 1;
4404
+ padding: var(--spacing-6) var(--spacing-4);
4405
+ text-align: center;
4406
+ gap: var(--spacing-3);
4407
+ }
4408
+
4409
+ .liveChat-feedback-thankyou-emoji {
4410
+ font-size: 48px;
4411
+ line-height: 1;
4412
+ }
4413
+
4414
+ .liveChat-feedback-thankyou h3 {
4415
+ margin: 0;
4416
+ font-size: var(--font-size-lg);
4417
+ font-weight: var(--font-weight-semibold);
4418
+ color: var(--text-primary);
4419
+ }
4420
+
4421
+ .liveChat-feedback-thankyou p {
4422
+ margin: 0;
4423
+ font-size: var(--font-size-sm);
4424
+ color: var(--text-secondary);
4425
+ }
4426
+
4427
+ .liveChat-feedback-done-btn {
4428
+ margin-top: var(--spacing-2);
4429
+ padding: var(--spacing-2) var(--spacing-5);
4430
+ border-radius: var(--radius-md);
4431
+ font-size: var(--font-size-sm);
4432
+ font-weight: var(--font-weight-medium);
4433
+ font-family: inherit;
4434
+ cursor: pointer;
4435
+ border: 1px solid var(--border-color);
4436
+ background: transparent;
4437
+ color: var(--text-primary);
4438
+ transition: all var(--transition-fast);
4439
+ }
4440
+
4441
+ .liveChat-feedback-done-btn:hover {
4442
+ background: var(--bg-secondary);
4443
+ }
4285
4444
  `;
4286
4445
 
4287
4446
  const liveChatCoreStyles = `
4288
4447
 
4448
+ .liveChat-widget * {
4449
+ font-weight: 500;
4450
+ }
4451
+
4289
4452
  .liveChat-launcher {
4290
4453
  position: fixed;
4291
4454
  z-index: var(--z-modal);
@@ -8216,11 +8379,28 @@
8216
8379
 
8217
8380
  this.ws = null;
8218
8381
  this.reconnectAttempts = 0;
8219
- this.maxReconnectAttempts = 5;
8220
- this.reconnectDelay = 1000;
8382
+ // Reconnect indefinitely with capped exponential backoff. The
8383
+ // previous 5-attempt hard cap meant ~31s of churn on a flaky network
8384
+ // permanently killed the live-chat connection until page refresh —
8385
+ // the dominant "messages don't arrive" symptom in production.
8386
+ this.reconnectBaseDelay = 1000;
8387
+ this.reconnectMaxDelay = 30_000;
8221
8388
  this.pingInterval = null;
8222
8389
  this.isConnected = false;
8223
8390
 
8391
+ // Heartbeat watchdog. The browser's onclose/onerror only fires when
8392
+ // it notices the TCP socket is dead — on stalled-but-not-closed
8393
+ // connections (NAT timeouts, sleeping mobile, some intermediaries)
8394
+ // that can take many minutes. We track the wall-clock time of the
8395
+ // last received frame and force a reconnect if nothing arrives
8396
+ // within the timeout. Server's protocol-level pings come every 54s
8397
+ // (pkg/websocket/client.go pingPeriod) and our app-level pings/pongs
8398
+ // every 30s, so 90s catches a dead pipe with one cycle of headroom.
8399
+ this.lastFrameAt = 0;
8400
+ this.heartbeatTimeoutMs = 90_000;
8401
+ this.heartbeatCheckIntervalMs = 15_000;
8402
+ this.heartbeatInterval = null;
8403
+
8224
8404
  // Event listeners
8225
8405
  this._listeners = new Map();
8226
8406
 
@@ -8244,6 +8424,8 @@
8244
8424
  return;
8245
8425
  }
8246
8426
 
8427
+ this._intentionallyClosed = false;
8428
+
8247
8429
  // Mock mode - simulate connection
8248
8430
  if (this.mock) {
8249
8431
  this.isConnected = true;
@@ -8275,13 +8457,15 @@
8275
8457
  */
8276
8458
  disconnect() {
8277
8459
  this.isConnected = false;
8278
- this.reconnectAttempts = this.maxReconnectAttempts; // Prevent reconnection
8460
+ this._intentionallyClosed = true; // Prevent reconnection
8279
8461
 
8280
8462
  if (this.pingInterval) {
8281
8463
  clearInterval(this.pingInterval);
8282
8464
  this.pingInterval = null;
8283
8465
  }
8284
8466
 
8467
+ this._stopHeartbeat();
8468
+
8285
8469
  if (this.ws) {
8286
8470
  this.ws.close();
8287
8471
  this.ws = null;
@@ -8293,6 +8477,35 @@
8293
8477
  }
8294
8478
  }
8295
8479
 
8480
+ _startHeartbeat() {
8481
+ this._stopHeartbeat();
8482
+ this.lastFrameAt = Date.now();
8483
+ this.heartbeatInterval = setInterval(() => {
8484
+ if (Date.now() - this.lastFrameAt > this.heartbeatTimeoutMs) {
8485
+ console.warn(
8486
+ `[WebSocket] No frames in ${this.heartbeatTimeoutMs}ms, forcing reconnect`
8487
+ );
8488
+ // Closing the socket fires onclose, which schedules a
8489
+ // reconnect through the normal path.
8490
+ this._stopHeartbeat();
8491
+ if (this.ws) {
8492
+ try {
8493
+ this.ws.close();
8494
+ } catch (_) {
8495
+ // ignore
8496
+ }
8497
+ }
8498
+ }
8499
+ }, this.heartbeatCheckIntervalMs);
8500
+ }
8501
+
8502
+ _stopHeartbeat() {
8503
+ if (this.heartbeatInterval) {
8504
+ clearInterval(this.heartbeatInterval);
8505
+ this.heartbeatInterval = null;
8506
+ }
8507
+ }
8508
+
8296
8509
  /**
8297
8510
  * Subscribe to events
8298
8511
  * @param {string} event - Event name
@@ -8342,6 +8555,8 @@
8342
8555
  console.log('[WebSocket] Connected');
8343
8556
  this.isConnected = true;
8344
8557
  this.reconnectAttempts = 0;
8558
+ this._intentionallyClosed = false;
8559
+ this._startHeartbeat();
8345
8560
  this._emit('connected', {});
8346
8561
 
8347
8562
  // Start ping interval to keep connection alive
@@ -8351,6 +8566,9 @@
8351
8566
  }
8352
8567
 
8353
8568
  _onMessage(event) {
8569
+ // Any inbound frame counts as a sign of life for the watchdog —
8570
+ // including pongs, message:new, typing, etc.
8571
+ this.lastFrameAt = Date.now();
8354
8572
  try {
8355
8573
  const data = JSON.parse(event.data);
8356
8574
  const { type, payload } = data;
@@ -8395,8 +8613,13 @@
8395
8613
  this.pingInterval = null;
8396
8614
  }
8397
8615
 
8616
+ this._stopHeartbeat();
8617
+
8398
8618
  this._emit('disconnected', { code: event.code, reason: event.reason });
8399
- this._scheduleReconnect();
8619
+ // Skip reconnect if disconnect() was called intentionally.
8620
+ if (!this._intentionallyClosed) {
8621
+ this._scheduleReconnect();
8622
+ }
8400
8623
  }
8401
8624
 
8402
8625
  _onError(error) {
@@ -8405,14 +8628,13 @@
8405
8628
  }
8406
8629
 
8407
8630
  _scheduleReconnect() {
8408
- if (this.reconnectAttempts >= this.maxReconnectAttempts) {
8409
- console.log('[WebSocket] Max reconnect attempts reached');
8410
- this._emit('reconnect_failed', {});
8411
- return;
8412
- }
8631
+ if (this._intentionallyClosed) return;
8413
8632
 
8414
8633
  this.reconnectAttempts++;
8415
- const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
8634
+ const delay = Math.min(
8635
+ this.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts - 1),
8636
+ this.reconnectMaxDelay
8637
+ );
8416
8638
  console.log(
8417
8639
  `[WebSocket] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`
8418
8640
  );
@@ -9431,11 +9653,12 @@
9431
9653
  </div>`;
9432
9654
  }
9433
9655
 
9434
- // Hide nav in chat and prechat views
9656
+ // Hide nav in chat, prechat and feedback views
9435
9657
  if (navContainer) {
9436
9658
  const hideNav =
9437
9659
  this.state.currentView === 'chat' ||
9438
- this.state.currentView === 'prechat';
9660
+ this.state.currentView === 'prechat' ||
9661
+ this.state.currentView === 'feedback';
9439
9662
  navContainer.style.display = hideNav ? 'none' : '';
9440
9663
  }
9441
9664
  }
@@ -11173,8 +11396,11 @@
11173
11396
  const sendIcon = `<iconify-icon icon="ph:paper-plane-right" width="20" height="20" style="flex-shrink: 0;"></iconify-icon>`;
11174
11397
  const caretIcon = `<iconify-icon icon="ph:caret-right" width="20" height="20" style="flex-shrink: 0;"></iconify-icon>`;
11175
11398
 
11176
- const responseTime =
11177
- this.state.responseTime || 'We typically reply within a few minutes';
11399
+ const isUnavailable = this.state.businessHoursState === 'offline' || this.state.businessHoursState === 'away';
11400
+ const buttonLabel = isUnavailable ? 'Leave us a message' : this.state.startButtonText;
11401
+ const buttonSubtext = isUnavailable
11402
+ ? "We'll get back to you when we're back"
11403
+ : (this.state.responseTime || 'We typically reply within a few minutes');
11178
11404
 
11179
11405
  const recentCardHtml = openConversation
11180
11406
  ? this._renderRecentMessageCard(openConversation)
@@ -11184,8 +11410,8 @@
11184
11410
  ${recentCardHtml}
11185
11411
  <button class="liveChat-home-message-btn">
11186
11412
  <div class="liveChat-home-continue-info">
11187
- <span class="liveChat-home-continue-label">${this.state.startButtonText}</span>
11188
- <span class="liveChat-home-message-subtext">${responseTime}</span>
11413
+ <span class="liveChat-home-continue-label">${buttonLabel}</span>
11414
+ <span class="liveChat-home-message-subtext">${buttonSubtext}</span>
11189
11415
  </div>
11190
11416
  ${sendIcon}
11191
11417
  </button>
@@ -11349,12 +11575,7 @@
11349
11575
  const feedbackBtn = this.element.querySelector('.liveChat-feedback-btn');
11350
11576
  if (feedbackBtn) {
11351
11577
  feedbackBtn.addEventListener('click', () => {
11352
- if (this.options.onFeedbackClick) {
11353
- this.state.setOpen(false);
11354
- this.options.onFeedbackClick();
11355
- } else if (this.state.urls?.feedback) {
11356
- window.open(this.state.urls.feedback, '_blank');
11357
- }
11578
+ this.state.setView('feedback');
11358
11579
  });
11359
11580
  }
11360
11581
 
@@ -11408,6 +11629,111 @@
11408
11629
  }
11409
11630
  }
11410
11631
 
11632
+ class FeedbackFormView {
11633
+ constructor(state, options = {}) {
11634
+ this.state = state;
11635
+ this.options = options;
11636
+ this.element = null;
11637
+ this._isSubmitting = false;
11638
+ this._selectedRating = null;
11639
+ }
11640
+
11641
+ render() {
11642
+ this.element = document.createElement('div');
11643
+ this.element.className = 'liveChat-view liveChat-feedback-view';
11644
+ this._renderForm();
11645
+ return this.element;
11646
+ }
11647
+
11648
+ _renderForm() {
11649
+ this.element.innerHTML = `
11650
+ <div class="liveChat-feedback-header">
11651
+ <button class="sdk-btn-icon liveChat-feedback-back-btn">
11652
+ <iconify-icon icon="ph:arrow-left" width="20" height="20"></iconify-icon>
11653
+ </button>
11654
+ <span class="liveChat-feedback-title">Leave us feedback</span>
11655
+ </div>
11656
+ <div class="liveChat-feedback-body">
11657
+ <p class="liveChat-feedback-prompt">Share your thoughts with us. We read every message.</p>
11658
+ <input
11659
+ type="text"
11660
+ class="liveChat-feedback-input"
11661
+ placeholder="Title"
11662
+ />
11663
+ <textarea
11664
+ class="liveChat-feedback-textarea"
11665
+ placeholder="Your feedback..."
11666
+ rows="5"
11667
+ ></textarea>
11668
+ <button class="liveChat-feedback-submit">Send feedback</button>
11669
+ </div>
11670
+ `;
11671
+ this._attachEvents();
11672
+ }
11673
+
11674
+ _renderThankYou() {
11675
+ this.element.innerHTML = `
11676
+ <div class="liveChat-feedback-header">
11677
+ <button class="sdk-btn-icon liveChat-feedback-back-btn">
11678
+ <iconify-icon icon="ph:arrow-left" width="20" height="20"></iconify-icon>
11679
+ </button>
11680
+ <span class="liveChat-feedback-title">Leave us feedback</span>
11681
+ </div>
11682
+ <div class="liveChat-feedback-thankyou">
11683
+ <span class="liveChat-feedback-thankyou-emoji">🙏</span>
11684
+ <h3>Thanks for your feedback!</h3>
11685
+ <p>We appreciate you taking the time to share your thoughts.</p>
11686
+ <button class="liveChat-feedback-done-btn">Done</button>
11687
+ </div>
11688
+ `;
11689
+ this.element.querySelector('.liveChat-feedback-back-btn').addEventListener('click', () => {
11690
+ this.state.setView('home');
11691
+ });
11692
+ this.element.querySelector('.liveChat-feedback-done-btn').addEventListener('click', () => {
11693
+ this.state.setView('home');
11694
+ });
11695
+ }
11696
+
11697
+ _attachEvents() {
11698
+ this.element.querySelector('.liveChat-feedback-back-btn').addEventListener('click', () => {
11699
+ this.state.setView('home');
11700
+ });
11701
+
11702
+ const submitBtn = this.element.querySelector('.liveChat-feedback-submit');
11703
+ submitBtn.addEventListener('click', async () => {
11704
+ if (this._isSubmitting) return;
11705
+ const title = this.element.querySelector('.liveChat-feedback-input').value.trim();
11706
+ const message = this.element.querySelector('.liveChat-feedback-textarea').value.trim();
11707
+ await this._submit(title, message);
11708
+ });
11709
+ }
11710
+
11711
+ async _submit(title, message) {
11712
+ this._isSubmitting = true;
11713
+ const submitBtn = this.element.querySelector('.liveChat-feedback-submit');
11714
+ if (submitBtn) {
11715
+ submitBtn.disabled = true;
11716
+ submitBtn.textContent = 'Sending...';
11717
+ }
11718
+
11719
+ try {
11720
+ if (this.options.onSubmitFeedback) {
11721
+ await this.options.onSubmitFeedback({ title, message });
11722
+ }
11723
+ } catch (e) {
11724
+ console.warn('[FeedbackFormView] Submit error:', e);
11725
+ }
11726
+
11727
+ this._renderThankYou();
11728
+ }
11729
+
11730
+ destroy() {
11731
+ if (this.element && this.element.parentNode) {
11732
+ this.element.parentNode.removeChild(this.element);
11733
+ }
11734
+ }
11735
+ }
11736
+
11411
11737
  class PreChatFormView {
11412
11738
  constructor(state, options = {}) {
11413
11739
  this.state = state;
@@ -11726,6 +12052,7 @@
11726
12052
  this.panel.registerView('messages', ConversationsView);
11727
12053
  this.panel.registerView('chat', ChatView);
11728
12054
  this.panel.registerView('prechat', PreChatFormView);
12055
+ this.panel.registerView('feedback', FeedbackFormView);
11729
12056
  this.panel.registerView('help', HelpView);
11730
12057
  this.panel.registerView('changelog', ChangelogView);
11731
12058
 
@@ -12081,13 +12408,34 @@
12081
12408
  this._wsUnsubscribers.push(
12082
12409
  this.wsService.on('conversation_closed', this._handleConversationClosed)
12083
12410
  );
12411
+ // Track first vs reconnect locally so we only backfill on reconnects.
12412
+ // First connect is right after _initWebSocket() and the surrounding
12413
+ // flow has already loaded fresh data — re-fetching would be wasted.
12414
+ let wsHasConnectedBefore = false;
12084
12415
  this._wsUnsubscribers.push(
12085
12416
  this.wsService.on('connected', () => {
12086
12417
  console.log('[LiveChatWidget] WebSocket connected');
12418
+ const isReconnect = wsHasConnectedBefore;
12419
+ wsHasConnectedBefore = true;
12087
12420
  if (this.LiveChatState.activeConversationId) {
12088
12421
  this.wsService.send('conversation:subscribe', {
12089
12422
  conversation_id: this.LiveChatState.activeConversationId,
12090
12423
  });
12424
+ // On reconnect, refetch the active conversation's messages.
12425
+ // The server doesn't replay events that fired during the WS
12426
+ // disconnect, so anything broadcast in the gap is otherwise
12427
+ // permanently lost from the customer's view until they
12428
+ // refresh the page.
12429
+ if (isReconnect) {
12430
+ this.fetchMessages(this.LiveChatState.activeConversationId).catch(
12431
+ (err) => {
12432
+ console.error(
12433
+ '[LiveChatWidget] Failed to backfill messages on reconnect:',
12434
+ err
12435
+ );
12436
+ }
12437
+ );
12438
+ }
12091
12439
  }
12092
12440
  })
12093
12441
  );