@subiz/widget-v4 4.1.214 → 4.1.216

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.
Files changed (42) hide show
  1. package/README.md +6 -0
  2. package/api.md +24 -8
  3. package/assets/img/bubble_default.svg +1 -0
  4. package/assets/less/widget.less +12 -0
  5. package/compiled_src/chatbox/ChatButton.js +19 -17
  6. package/compiled_src/lib.js +2 -15
  7. package/compiled_src/store/api.js +0 -1
  8. package/compiled_src/store/store.js +68 -1
  9. package/dist/{1a12a372880.278.js → 1a12ecfb6db.278.js} +1 -1
  10. package/dist/{1a12a372880.webview.js → 1a12ecfb6db.app.js} +1 -1
  11. package/dist/{1a12a372880.chat.html → 1a12ecfb6db.chat.html} +1 -1
  12. package/dist/1a12ecfb6db.chat.js +1 -0
  13. package/dist/{1a12a372880.form.html → 1a12ecfb6db.form.html} +1 -1
  14. package/dist/1a12ecfb6db.form.js +1 -0
  15. package/dist/{1a12a372880.index.html → 1a12ecfb6db.index.html} +1 -1
  16. package/dist/{1a12a372880.library.html → 1a12ecfb6db.library.html} +1 -1
  17. package/dist/1a12ecfb6db.library.js +1 -0
  18. package/dist/{1a12a372880.preview.html → 1a12ecfb6db.preview.html} +1 -1
  19. package/dist/1a12ecfb6db.preview.js +1 -0
  20. package/dist/1a12ecfb6db.webview.js +1 -0
  21. package/dist/HASH +1 -0
  22. package/dist/assets/img/{bubble_default.7d5e4ab.svg → bubble_default.6f6eec8.svg} +1 -0
  23. package/dist/screenshoot.js +90 -35
  24. package/package.json +1 -1
  25. package/src/chatbox/ChatButton.js +22 -20
  26. package/src/lib.js +2 -18
  27. package/src/store/api.js +0 -3
  28. package/src/store/store.js +52 -0
  29. package/up +18 -0
  30. package/dist/.HASH +0 -1
  31. package/dist/1a12a372880.app.js +0 -1
  32. package/dist/1a12a372880.chat.js +0 -1
  33. package/dist/1a12a372880.form.js +0 -1
  34. package/dist/1a12a372880.library.js +0 -1
  35. package/dist/1a12a372880.preview.js +0 -1
  36. /package/dist/{1a12a372880.127.js → 1a12ecfb6db.127.js} +0 -0
  37. /package/dist/{1a12a372880.132.js → 1a12ecfb6db.132.js} +0 -0
  38. /package/dist/{1a12a372880.185.js → 1a12ecfb6db.185.js} +0 -0
  39. /package/dist/{1a12a372880.27.js → 1a12ecfb6db.27.js} +0 -0
  40. /package/dist/{1a12a372880.339.js → 1a12ecfb6db.339.js} +0 -0
  41. /package/dist/{1a12a372880.459.js → 1a12ecfb6db.459.js} +0 -0
  42. /package/dist/{1a12a372880.490.js → 1a12ecfb6db.490.js} +0 -0
package/README.md CHANGED
@@ -6,6 +6,12 @@ $ npm run dev
6
6
 
7
7
  visit: http://localhost:8082/?account_id=acqcsmrppbftadjzxnvo
8
8
 
9
+ The fallback button keeps the configured width and height and has an opaque
10
+ white backing. Each dimension defaults to 70px only when it is not configured.
11
+
12
+ Masks use data URLs rather than blob URLs. If data URLs are blocked, the widget
13
+ tries the original image URL with CORS before using the fallback button.
14
+
9
15
  ### To test widget in injection mode
10
16
 
11
17
  ``` sh
package/api.md CHANGED
@@ -1264,6 +1264,7 @@ nghiệp vụ của form do website thực hiện.
1264
1264
  subiz('waitReady', async function () {
1265
1265
  try {
1266
1266
  const result = await subiz('getFormStatus', 'website_contact')
1267
+ // result.values: [{key: 'fullname', label: 'Họ tên', value: 'Nguyễn Văn An'}, ...]
1267
1268
  if (result.submitted) document.getElementById('contact-form').hidden = true
1268
1269
  } catch (error) {
1269
1270
  // Không ẩn form khi kiểm tra thất bại.
@@ -1271,11 +1272,26 @@ subiz('waitReady', async function () {
1271
1272
  })
1272
1273
  ```
1273
1274
 
1274
- `getFormStatus(formId)` trả `{submitted, status, conversation_id?}`, chỉ hỗ trợ ID
1275
- website không bắt đầu bằng `cp`. Cả hội thoại đã kết thúc cũng được tính.
1276
- `status: 'found'` nghĩa là tìm thấy lần gửi; `'not_found_in_available_history'`
1277
- không khẳng định khách chưa từng gửi. Service hiện chỉ đọc tối đa 200 dòng lịch sử
1278
- khách trước khi lọc theo form, sau đó trả tối đa 6 hội thoại; cache và việc xóa dữ
1279
- liệu cũng ảnh hưởng kết quả. Lần gửi theo luồng cũ dùng hostname làm nguồn hoặc
1280
- CF7 ticket chưa được tra cứu bằng API này. Khách cần được nhận diện lại bằng cùng
1281
- phiên widget; không đảm bảo khi xóa dữ liệu trình duyệt hoặc đổi thiết bị.
1275
+ `getFormStatus(formId)` đọc qua store KV/LSDB của widget, không gọi API trạng thái
1276
+ hay lấy hội thoại. Sau khi submit thành công, SDK lưu dấu đã gửi và nội dung form
1277
+ trong 7 ngày, tách theo account, ID khách và ID form. Bản ghi gồm mã hội thoại,
1278
+ thời điểm hết hạn và `values` của lần gửi thành công gần nhất, theo dạng
1279
+ `[{key, label, value}]` với giá trị đã chuẩn hóa thành chuỗi như khi gửi.
1280
+ Đọc trạng thái không gia hạn thời gian lưu. Lần gửi thất bại không ghi đè dữ liệu cũ.
1281
+
1282
+ Kết quả: `{submitted: true, status: 'found', conversation_id, values}` nếu còn hạn;
1283
+ `{submitted: false, status: 'not_found'}` nếu chưa lưu; `status: 'expired'` khi
1284
+ hết hạn (đồng thời xóa bản ghi); `status: 'storage_unavailable'` khi trình duyệt
1285
+ không cho truy cập localStorage. Lỗi lưu localStorage không biến một submit đã
1286
+ thành công thành thất bại. Website nên ẩn form ngay sau khi submit thành công.
1287
+
1288
+ SDK bỏ qua sự kiện realtime của hội thoại kênh form trước khi lấy chi tiết hội
1289
+ thoại; không mở hội thoại form trong chat widget. Luồng chat thông thường giữ
1290
+ nguyên. Trạng thái chỉ có trên cùng website/trình duyệt và cùng ID khách, không
1291
+ đồng bộ thiết bị khác, mất khi xóa localStorage. Các lần gửi trước khi bổ sung
1292
+ cơ chế lưu này không được tự khôi phục từ server. Bản ghi cũ chỉ lưu dấu đã gửi
1293
+ vẫn trả `submitted: true` nếu còn hạn, nhưng `values: []` vì chưa lưu nội dung.
1294
+
1295
+ SDK không truy cập localStorage trực tiếp: store dùng `kv.put/match/del` và
1296
+ LSDB hiện có. Khi widget dùng chế độ in-memory hoặc trình duyệt chặn lưu trữ,
1297
+ trạng thái có thể chỉ giữ trong bộ nhớ của trang và mất khi tải lại.
@@ -7,6 +7,7 @@
7
7
  </style>
8
8
  </defs>
9
9
  <title>Asset 5</title>
10
+ <circle cx="30" cy="30" r="30" fill="white"/>
10
11
  <g id="Layer_2" data-name="Layer 2">
11
12
  <g id="Layer_1-2" data-name="Layer 1">
12
13
  <g>
@@ -438,6 +438,18 @@
438
438
  // min-height: @desktop-chat-button-height;
439
439
  }
440
440
 
441
+ .chat-button--fallback {
442
+ .chat-button--img-container {
443
+ background-size: 100% 100%;
444
+ }
445
+
446
+ .chat-button--img {
447
+ width: 100%;
448
+ min-width: 0;
449
+ height: 100%;
450
+ }
451
+ }
452
+
441
453
  .chat-button.chat-button--left {
442
454
  float: left;
443
455
  }
@@ -400,18 +400,10 @@ class ChatButton extends _common.Com {
400
400
  this.readyGate.close();
401
401
  const [loaded, mask] = await Promise.all([Promise.all(appearance.urls.filter(url => url !== appearance.maskAsset).map(url => preloadButtonImage(url))), appearance.maskAsset ? prepareButtonMask(appearance.maskAsset) : Promise.resolve(null)]);
402
402
  const stale = () => !this._isMounted || this._buttonImageVersion !== version;
403
- if (stale()) {
404
- if (mask) URL.revokeObjectURL(mask);
405
- return;
406
- }
403
+ if (stale()) return;
407
404
  const fallback = loaded.some(success => !success) || !!appearance.maskAsset && !mask;
408
405
  const ready = !fallback || (await preloadButtonImage(defaultBubble));
409
- if (stale()) {
410
- if (mask) URL.revokeObjectURL(mask);
411
- return;
412
- }
413
- if (this._buttonMaskUrl) URL.revokeObjectURL(this._buttonMaskUrl);
414
- this._buttonMaskUrl = mask;
406
+ if (stale()) return;
415
407
  if (this._buttonElement) window.getComputedStyle(this._buttonElement).transform;
416
408
  this.setState({
417
409
  ready,
@@ -422,7 +414,6 @@ class ChatButton extends _common.Com {
422
414
  this.readyGate.open();
423
415
  }
424
416
  componentWillUnmount() {
425
- if (this._buttonMaskUrl) URL.revokeObjectURL(this._buttonMaskUrl);
426
417
  if (this._animateTimeout) clearTimeout(this._animateTimeout);
427
418
  }
428
419
  renderButton() {
@@ -437,11 +428,12 @@ class ChatButton extends _common.Com {
437
428
  imgwidth,
438
429
  imgheight
439
430
  } = appearance;
431
+ const fallback = this.state.readyImageKey === appearance.key && this.state.fallbackButtonImage;
440
432
  if (appearance.maskAsset) {
441
433
  maskurl = this.state.readyImageKey === appearance.key ? this.state.buttonMaskUrl : '';
442
434
  if (imgsrc === appearance.maskAsset) imgsrc = maskurl || '';
443
435
  }
444
- if (this.state.readyImageKey === appearance.key && this.state.fallbackButtonImage) {
436
+ if (fallback) {
445
437
  imgsrc = imgurl = defaultBubble;
446
438
  maskurl = '';
447
439
  }
@@ -501,7 +493,11 @@ class ChatButton extends _common.Com {
501
493
  mask-size: cover !important;
502
494
  `;
503
495
  }
496
+ if (fallback) {
497
+ imgcontainerstyle += `width: ${imgwidth}px !important; height: ${imgheight}px !important;`;
498
+ }
504
499
  let cls = 'chat-button chat-button--' + position;
500
+ if (fallback) cls += ' chat-button--fallback';
505
501
  let whiteBgStyle = `
506
502
  width: ${imgwidth - 4}px !important;
507
503
  height: ${imgheight - 4}px !important;
@@ -624,13 +620,19 @@ function preloadButtonImage(src, {
624
620
  });
625
621
  }
626
622
  async function prepareButtonMask(src) {
627
- let url;
628
623
  try {
629
624
  const response = await fetch(src);
630
625
  if (!response.ok) return null;
631
- url = URL.createObjectURL(await response.blob());
632
- if (await preloadButtonImage(url)) return url;
626
+ const blob = await response.blob();
627
+ const url = await new Promise(resolve => {
628
+ const reader = new FileReader();
629
+ reader.onload = () => resolve(reader.result);
630
+ reader.onerror = reader.onabort = () => resolve('');
631
+ reader.readAsDataURL(blob);
632
+ });
633
+ if (url && (await preloadButtonImage(url))) return url;
633
634
  } catch (_) {}
634
- if (url) URL.revokeObjectURL(url);
635
- return null;
635
+ return (await preloadButtonImage(src, {
636
+ crossOrigin: true
637
+ })) ? src : null;
636
638
  }
@@ -233,12 +233,12 @@ class Widget {
233
233
  return this.store.sendConversion(formid, values, impressiontoken);
234
234
  }
235
235
  const payload = formRequest(formid, values, options);
236
- return formResult(await this.store.api.submitWebsiteForm(formid, payload), 'submit');
236
+ return this.store.submitWebsiteForm(formid, payload);
237
237
  }
238
238
  async getFormStatus(formid) {
239
239
  validateWebsiteFormId(formid);
240
240
  await this.store.readyGate.entry();
241
- return formResult(await this.store.api.getWebsiteFormStatus(formid), 'status');
241
+ return this.store.getFormStatus(formid);
242
242
  }
243
243
  startConvo(req) {
244
244
  req = _index.default.cloneDeep(req) || {};
@@ -378,17 +378,4 @@ function formRequest(formid, fields, options = {}) {
378
378
  };
379
379
  })
380
380
  };
381
- }
382
- function formResult(response, operation) {
383
- const body = response && response.body;
384
- const detail = response && response.error || body && body.error;
385
- const status = response && response.code;
386
- const valid = operation === 'submit' ? body && typeof body.conversation_id === 'string' && body.conversation_id && typeof body.submission_id === 'string' && body.submission_id : body && typeof body.submitted === 'boolean' && typeof body.status === 'string';
387
- if (detail || !(status >= 200 && status < 300) || !valid) {
388
- const error = new Error('Subiz form request failed');
389
- error.code = detail && detail.code || 'form_request_failed';
390
- error.status = status;
391
- throw error;
392
- }
393
- return body;
394
381
  }
@@ -65,7 +65,6 @@ class _default {
65
65
  _defineProperty(this, "sendEvent", data => this.apireq.addQuery('shorten', true).addQuery('x-referer', _index.default.get(location, 'origin', '')).post(`/accounts/${this.accid}/users/${this.uid}/events`, data));
66
66
  _defineProperty(this, "websiteFormRequest", () => this.apireq.setBaseUrl(this.ApiURL.replace(/\/4\.[01]\/?$/, '/4.1')));
67
67
  _defineProperty(this, "submitWebsiteForm", (formid, payload) => this.websiteFormRequest().post(`/accounts/${this.accid}/external-forms/${encodeURIComponent(formid)}/submissions`, payload));
68
- _defineProperty(this, "getWebsiteFormStatus", formid => this.websiteFormRequest().get(`/accounts/${this.accid}/external-forms/${encodeURIComponent(formid)}/status`));
69
68
  _defineProperty(this, "submitForm", (form_id, page_url, device, data, user) => this.apireq.addQuery('x-referer', _index.default.get(location, 'origin', '')).post(`/accounts/${this.accid}/users/${this.uid}/events`, {
70
69
  by: {
71
70
  id: this.uid,
@@ -881,6 +881,56 @@ class Store {
881
881
  this.reconnectGate.open();
882
882
  }
883
883
  }
884
+ async submitWebsiteForm(formid, payload) {
885
+ await this.readyGate.entry();
886
+ const key = websiteFormStorageKey(this.api, formid);
887
+ const kv = this.kv;
888
+ const values = _index.default.cloneDeep(payload.values);
889
+ const result = formResult(await this.api.submitWebsiteForm(formid, payload));
890
+ try {
891
+ kv.put(key, {
892
+ conversation_id: result.conversation_id,
893
+ values,
894
+ expires_at: _index.default.Date.now() + 7 * 24 * 60 * 60 * 1000
895
+ });
896
+ } catch (_) {}
897
+ return result;
898
+ }
899
+ getFormStatus(formid) {
900
+ const key = websiteFormStorageKey(this.api, formid);
901
+ try {
902
+ const receipt = this.kv.match(key);
903
+ if (!receipt) return {
904
+ submitted: false,
905
+ status: 'not_found'
906
+ };
907
+ if (!Number.isFinite(receipt.expires_at) || typeof receipt.conversation_id !== 'string' || !receipt.conversation_id) {
908
+ this.kv.del(key);
909
+ return {
910
+ submitted: false,
911
+ status: 'not_found'
912
+ };
913
+ }
914
+ if (receipt.expires_at <= _index.default.Date.now()) {
915
+ this.kv.del(key);
916
+ return {
917
+ submitted: false,
918
+ status: 'expired'
919
+ };
920
+ }
921
+ return {
922
+ submitted: true,
923
+ status: 'found',
924
+ conversation_id: receipt.conversation_id,
925
+ values: Array.isArray(receipt.values) ? _index.default.cloneDeep(receipt.values) : []
926
+ };
927
+ } catch (_) {
928
+ return {
929
+ submitted: false,
930
+ status: 'storage_unavailable'
931
+ };
932
+ }
933
+ }
884
934
  async submitForm(formid, data, user) {
885
935
  await this.readyGate.entry();
886
936
  if (user) await this.setUser(user);
@@ -1008,6 +1058,7 @@ class Store {
1008
1058
  async _onWs(ev) {
1009
1059
  if (this._die) return;
1010
1060
  if (!ev || !ev.type) return;
1061
+ if (_index.default.get(ev, 'touchpoint.channel') === 'form' || _index.default.get(ev, 'data.conversation.touchpoint.channel') === 'form') return;
1011
1062
  ev.created = _index.default.getMs(ev.created);
1012
1063
  if (ev.type === 'user_info_updated') {
1013
1064
  return;
@@ -1757,4 +1808,20 @@ var xhrsend = function (method, url, body, cb) {
1757
1808
  if (body) request.setRequestHeader('content-type', 'text/plain');
1758
1809
  request.send(body);
1759
1810
  };
1760
- const dieErr = new Error('sdk unloaded');
1811
+ const dieErr = new Error('sdk unloaded');
1812
+ function websiteFormStorageKey(api, formid) {
1813
+ return 'sbz_form_submitted:' + _index.default.stringify([api.accid, api.uid, formid]);
1814
+ }
1815
+ function formResult(response) {
1816
+ const body = response && response.body;
1817
+ const detail = response && response.error || body && body.error;
1818
+ const status = response && response.code;
1819
+ const valid = body && typeof body.conversation_id === 'string' && body.conversation_id && typeof body.submission_id === 'string' && body.submission_id;
1820
+ if (detail || !(status >= 200 && status < 300) || !valid) {
1821
+ const error = new Error('Subiz form request failed');
1822
+ error.code = detail && detail.code || 'form_request_failed';
1823
+ error.status = status;
1824
+ throw error;
1825
+ }
1826
+ return body;
1827
+ }