@subiz/widget-v4 4.1.213 → 4.1.215

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 (31) hide show
  1. package/api.md +69 -0
  2. package/compiled_src/lib.js +39 -30
  3. package/compiled_src/store/api.js +2 -0
  4. package/compiled_src/store/store.js +65 -1
  5. package/dist/.HASH +1 -0
  6. package/dist/{1a1101c351b.app.js → 1a12a5ed6a9.app.js} +1 -1
  7. package/dist/{1a1101c351b.chat.html → 1a12a5ed6a9.chat.html} +1 -1
  8. package/dist/{1a1101c351b.chat.js → 1a12a5ed6a9.chat.js} +1 -1
  9. package/dist/{1a1101c351b.form.html → 1a12a5ed6a9.form.html} +1 -1
  10. package/dist/1a12a5ed6a9.form.js +1 -0
  11. package/dist/{1a1101c351b.index.html → 1a12a5ed6a9.index.html} +1 -1
  12. package/dist/{1a1101c351b.library.html → 1a12a5ed6a9.library.html} +1 -1
  13. package/dist/{1a1101c351b.library.js → 1a12a5ed6a9.library.js} +1 -1
  14. package/dist/{1a1101c351b.preview.html → 1a12a5ed6a9.preview.html} +1 -1
  15. package/dist/{1a1101c351b.preview.js → 1a12a5ed6a9.preview.js} +1 -1
  16. package/dist/{1a1101c351b.webview.js → 1a12a5ed6a9.webview.js} +1 -1
  17. package/dist/screenshoot.js +104 -29
  18. package/package.json +1 -1
  19. package/src/lib.js +38 -24
  20. package/src/store/api.js +6 -0
  21. package/src/store/store.js +51 -0
  22. package/dist/1a1101c351b.form.js +0 -1
  23. package/dist/HASH +0 -1
  24. /package/dist/{1a1101c351b.127.js → 1a12a5ed6a9.127.js} +0 -0
  25. /package/dist/{1a1101c351b.132.js → 1a12a5ed6a9.132.js} +0 -0
  26. /package/dist/{1a1101c351b.185.js → 1a12a5ed6a9.185.js} +0 -0
  27. /package/dist/{1a1101c351b.27.js → 1a12a5ed6a9.27.js} +0 -0
  28. /package/dist/{1a1101c351b.278.js → 1a12a5ed6a9.278.js} +0 -0
  29. /package/dist/{1a1101c351b.339.js → 1a12a5ed6a9.339.js} +0 -0
  30. /package/dist/{1a1101c351b.459.js → 1a12a5ed6a9.459.js} +0 -0
  31. /package/dist/{1a1101c351b.490.js → 1a12a5ed6a9.490.js} +0 -0
package/api.md CHANGED
@@ -1222,3 +1222,72 @@ subiz('event', 'product_viewed', {
1222
1222
  name: 'Bàn phím cơ không dây',
1223
1223
  })
1224
1224
  ```
1225
+
1226
+ ## Form website: tạo hội thoại và kiểm tra lần gửi trước
1227
+
1228
+ ```js
1229
+ subiz('waitReady', async function () {
1230
+ const result = await subiz('submitForm', 'website_contact', [
1231
+ {key: 'fullname', label: 'Họ tên', value: 'Nguyễn Văn An'},
1232
+ {key: 'phones', value: '0901234567'},
1233
+ ], {
1234
+ name: 'Đăng ký tư vấn',
1235
+ // page_url: 'https://example.com/contact',
1236
+ // submission_id: 'registration-123',
1237
+ })
1238
+ // result: {conversation_id, submission_id}
1239
+ })
1240
+ ```
1241
+
1242
+ Chữ ký: `submitForm(formId, fields, options?)`. Tham số thứ hai luôn là mảng.
1243
+ `label` mặc định bằng `key`. Giá trị string, number, boolean được gửi dưới dạng
1244
+ chuỗi; null/undefined thành chuỗi rỗng. `options.name` mặc định bằng formId,
1245
+ `page_url` bằng URL hiện tại, `submission_id` được SDK tự sinh.
1246
+
1247
+ SDK chờ phiên khách sẵn sàng và dùng xác thực hiện có. API lỗi hoặc phản hồi sai
1248
+ định dạng sẽ reject Promise, lỗi có `code` và `status`. Không tự retry submit:
1249
+ `submission_id` chưa chống tạo trùng. Website nên khóa nút trong khi gửi.
1250
+ Khi SDK chưa tải, loader chỉ xếp hàng lệnh và chưa trả Promise; dùng `waitReady`
1251
+ như ví dụ để gọi `await` an toàn.
1252
+
1253
+ ID bắt đầu bằng `cp` vẫn dùng luồng popup/campaign cũ, giữ nguyên kiểu kết quả cũ;
1254
+ options mới không áp dụng cho nhánh này. ID website khác `cp` chuyển sang API form
1255
+ mới: tạo hội thoại kênh form, không tự cập nhật thuộc tính hồ sơ khách. Backend mới
1256
+ phải được deploy trước khi phát hành SDK.
1257
+
1258
+ Giới hạn: ID form và key dài 1–64 ký tự ASCII chữ/số/`_`/`-`, bắt đầu bằng chữ/số;
1259
+ 1–50 trường; label/tên form tối đa 200 byte, giá trị 4096 byte, URL HTTP(S) 500 byte,
1260
+ toàn bộ JSON 32 KiB. Ít nhất một giá trị không rỗng. Kiểm tra bắt buộc/email theo
1261
+ nghiệp vụ của form do website thực hiện.
1262
+
1263
+ ```js
1264
+ subiz('waitReady', async function () {
1265
+ try {
1266
+ const result = await subiz('getFormStatus', 'website_contact')
1267
+ if (result.submitted) document.getElementById('contact-form').hidden = true
1268
+ } catch (error) {
1269
+ // Không ẩn form khi kiểm tra thất bại.
1270
+ }
1271
+ })
1272
+ ```
1273
+
1274
+ `getFormStatus(formId)` đọc qua store KV/LSDB của widget, không gọi API trạng thái
1275
+ hay lấy hội thoại. Sau khi submit thành công, SDK lưu dấu đã gửi trong 7 ngày,
1276
+ tách theo account, ID khách và ID form. Chỉ lưu mã hội thoại và thời điểm hết hạn,
1277
+ không lưu các giá trị khách nhập. Đọc trạng thái không gia hạn thời gian lưu.
1278
+
1279
+ Kết quả: `{submitted: true, status: 'found', conversation_id}` nếu còn hạn;
1280
+ `{submitted: false, status: 'not_found'}` nếu chưa lưu; `status: 'expired'` khi
1281
+ hết hạn (đồng thời xóa bản ghi); `status: 'storage_unavailable'` khi trình duyệt
1282
+ không cho truy cập localStorage. Lỗi lưu localStorage không biến một submit đã
1283
+ thành công thành thất bại. Website nên ẩn form ngay sau khi submit thành công.
1284
+
1285
+ 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
1286
+ thoại; không mở hội thoại form trong chat widget. Luồng chat thông thường giữ
1287
+ 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
1288
+ đồng bộ thiết bị khác, mất khi xóa localStorage. Các lần gửi trước khi bổ sung
1289
+ cơ chế lưu này không được tự khôi phục từ server.
1290
+
1291
+ SDK không truy cập localStorage trực tiếp: store dùng `kv.put/match/del` và
1292
+ LSDB hiện có. Khi widget dùng chế độ in-memory hoặc trình duyệt chặn lưu trữ,
1293
+ trạng thái có thể chỉ giữ trong bộ nhớ của trang và mất khi tải lại.
@@ -199,8 +199,9 @@ class Widget {
199
199
  this.impressionTokens[formid] = impressiontoken;
200
200
  return impressiontoken;
201
201
  }
202
- async submitForm(formid, values) {
203
- if (!formid || !values) return;
202
+ async submitForm(formid, values, options = {}) {
203
+ if (!formid || !values) throw new TypeError('submitForm requires a form ID and fields');
204
+ await this.store.readyGate.entry();
204
205
  if (_index.default.startsWith(formid, 'cp')) {
205
206
  let storeuser = _index.default.cloneDeep(this.store.matchUser());
206
207
  let user = {
@@ -231,33 +232,13 @@ class Widget {
231
232
  this.store.setUserDiff(user);
232
233
  return this.store.sendConversion(formid, values, impressiontoken);
233
234
  }
234
- values = _index.default.filter(values, field => {
235
- return field.key && field.value;
236
- });
237
- let text = 'Khách điền form\n';
238
- _index.default.map(values, field => {
239
- text += '+ ' + field.key + ': ' + field.value + '\n';
240
- });
241
- return this.store.startConvo({
242
- subject: 'Khách điền form',
243
- touchpoint: {
244
- channel: 'form',
245
- source: window.location.hostname,
246
- id: Date.now() + '_' + _index.default.randomString()
247
- },
248
- form_submission: {
249
- id: formid,
250
- values: values
251
- },
252
- message_sent: {
253
- data: {
254
- message: {
255
- format: 'plaintext',
256
- text: text
257
- }
258
- }
259
- }
260
- });
235
+ const payload = formRequest(formid, values, options);
236
+ return this.store.submitWebsiteForm(formid, payload);
237
+ }
238
+ async getFormStatus(formid) {
239
+ validateWebsiteFormId(formid);
240
+ await this.store.readyGate.entry();
241
+ return this.store.getFormStatus(formid);
261
242
  }
262
243
  startConvo(req) {
263
244
  req = _index.default.cloneDeep(req) || {};
@@ -369,4 +350,32 @@ class Widget {
369
350
  }
370
351
  }
371
352
  window.__sbz_WIDGET = Widget;
372
- var _default = exports.default = Widget;
353
+ var _default = exports.default = Widget;
354
+ function validateWebsiteFormId(formid) {
355
+ if (typeof formid !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(formid)) {
356
+ throw new TypeError('Form ID must contain 1-64 letters, digits, underscores or hyphens');
357
+ }
358
+ if (formid.startsWith('cp')) throw new TypeError('Form IDs starting with cp are reserved for Subiz campaigns');
359
+ }
360
+ function formRequest(formid, fields, options = {}) {
361
+ validateWebsiteFormId(formid);
362
+ if (!Array.isArray(fields) || !fields.length) throw new TypeError('Form fields must be a nonempty array');
363
+ if (!options || typeof options !== 'object' || Array.isArray(options)) throw new TypeError('Form options must be an object');
364
+ return {
365
+ form_name: options.name === undefined ? formid : options.name,
366
+ page_url: options.page_url === undefined ? window.location.href : options.page_url,
367
+ submission_id: options.submission_id === undefined ? _index.default.randomString(36) : options.submission_id,
368
+ values: fields.map(field => {
369
+ if (!field || typeof field.key !== 'string') throw new TypeError('Each field requires a key');
370
+ const value = field.value;
371
+ if (value != null && !['string', 'number', 'boolean'].includes(typeof value)) {
372
+ throw new TypeError('Form field values must be strings, numbers or booleans');
373
+ }
374
+ return {
375
+ key: field.key,
376
+ label: field.label === undefined ? field.key : field.label,
377
+ value: value == null ? '' : String(value)
378
+ };
379
+ })
380
+ };
381
+ }
@@ -63,6 +63,8 @@ class _default {
63
63
  _defineProperty(this, "getConversation", cid => this.apireq.get(`/accounts/${this.accid}/conversations/${cid}`));
64
64
  _defineProperty(this, "getLocaleContent", locale => this.apireq.get(`/accounts/${this.accid}/languages/${locale}`));
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
+ _defineProperty(this, "websiteFormRequest", () => this.apireq.setBaseUrl(this.ApiURL.replace(/\/4\.[01]\/?$/, '/4.1')));
67
+ _defineProperty(this, "submitWebsiteForm", (formid, payload) => this.websiteFormRequest().post(`/accounts/${this.accid}/external-forms/${encodeURIComponent(formid)}/submissions`, payload));
66
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`, {
67
69
  by: {
68
70
  id: this.uid,
@@ -881,6 +881,53 @@ 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 result = formResult(await this.api.submitWebsiteForm(formid, payload));
889
+ try {
890
+ kv.put(key, {
891
+ conversation_id: result.conversation_id,
892
+ expires_at: _index.default.Date.now() + 7 * 24 * 60 * 60 * 1000
893
+ });
894
+ } catch (_) {}
895
+ return result;
896
+ }
897
+ getFormStatus(formid) {
898
+ const key = websiteFormStorageKey(this.api, formid);
899
+ try {
900
+ const receipt = this.kv.match(key);
901
+ if (!receipt) return {
902
+ submitted: false,
903
+ status: 'not_found'
904
+ };
905
+ if (!Number.isFinite(receipt.expires_at) || typeof receipt.conversation_id !== 'string' || !receipt.conversation_id) {
906
+ this.kv.del(key);
907
+ return {
908
+ submitted: false,
909
+ status: 'not_found'
910
+ };
911
+ }
912
+ if (receipt.expires_at <= _index.default.Date.now()) {
913
+ this.kv.del(key);
914
+ return {
915
+ submitted: false,
916
+ status: 'expired'
917
+ };
918
+ }
919
+ return {
920
+ submitted: true,
921
+ status: 'found',
922
+ conversation_id: receipt.conversation_id
923
+ };
924
+ } catch (_) {
925
+ return {
926
+ submitted: false,
927
+ status: 'storage_unavailable'
928
+ };
929
+ }
930
+ }
884
931
  async submitForm(formid, data, user) {
885
932
  await this.readyGate.entry();
886
933
  if (user) await this.setUser(user);
@@ -1008,6 +1055,7 @@ class Store {
1008
1055
  async _onWs(ev) {
1009
1056
  if (this._die) return;
1010
1057
  if (!ev || !ev.type) return;
1058
+ if (_index.default.get(ev, 'touchpoint.channel') === 'form' || _index.default.get(ev, 'data.conversation.touchpoint.channel') === 'form') return;
1011
1059
  ev.created = _index.default.getMs(ev.created);
1012
1060
  if (ev.type === 'user_info_updated') {
1013
1061
  return;
@@ -1757,4 +1805,20 @@ var xhrsend = function (method, url, body, cb) {
1757
1805
  if (body) request.setRequestHeader('content-type', 'text/plain');
1758
1806
  request.send(body);
1759
1807
  };
1760
- const dieErr = new Error('sdk unloaded');
1808
+ const dieErr = new Error('sdk unloaded');
1809
+ function websiteFormStorageKey(api, formid) {
1810
+ return 'sbz_form_submitted:' + _index.default.stringify([api.accid, api.uid, formid]);
1811
+ }
1812
+ function formResult(response) {
1813
+ const body = response && response.body;
1814
+ const detail = response && response.error || body && body.error;
1815
+ const status = response && response.code;
1816
+ const valid = body && typeof body.conversation_id === 'string' && body.conversation_id && typeof body.submission_id === 'string' && body.submission_id;
1817
+ if (detail || !(status >= 200 && status < 300) || !valid) {
1818
+ const error = new Error('Subiz form request failed');
1819
+ error.code = detail && detail.code || 'form_request_failed';
1820
+ error.status = status;
1821
+ throw error;
1822
+ }
1823
+ return body;
1824
+ }
package/dist/.HASH ADDED
@@ -0,0 +1 @@
1
+ 1a12a5ed6a9