@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
@@ -4586,6 +4586,8 @@ let trackResponseFn = (param, done) => {
4586
4586
  _defineProperty(this, "getConversation", cid => this.apireq.get(`/accounts/${this.accid}/conversations/${cid}`));
4587
4587
  _defineProperty(this, "getLocaleContent", locale => this.apireq.get(`/accounts/${this.accid}/languages/${locale}`));
4588
4588
  _defineProperty(this, "sendEvent", data => this.apireq.addQuery('shorten', true).addQuery('x-referer', util/* default */.A.get(location, 'origin', '')).post(`/accounts/${this.accid}/users/${this.uid}/events`, data));
4589
+ _defineProperty(this, "websiteFormRequest", () => this.apireq.setBaseUrl(this.ApiURL.replace(/\/4\.[01]\/?$/, '/4.1')));
4590
+ _defineProperty(this, "submitWebsiteForm", (formid, payload) => this.websiteFormRequest().post(`/accounts/${this.accid}/external-forms/${encodeURIComponent(formid)}/submissions`, payload));
4589
4591
  _defineProperty(this, "submitForm", (form_id, page_url, device, data, user) => this.apireq.addQuery('x-referer', util/* default */.A.get(location, 'origin', '')).post(`/accounts/${this.accid}/users/${this.uid}/events`, {
4590
4592
  by: {
4591
4593
  id: this.uid,
@@ -6577,6 +6579,53 @@ class Store {
6577
6579
  this.reconnectGate.open();
6578
6580
  }
6579
6581
  }
6582
+ async submitWebsiteForm(formid, payload) {
6583
+ await this.readyGate.entry();
6584
+ const key = websiteFormStorageKey(this.api, formid);
6585
+ const kv = this.kv;
6586
+ const result = formResult(await this.api.submitWebsiteForm(formid, payload));
6587
+ try {
6588
+ kv.put(key, {
6589
+ conversation_id: result.conversation_id,
6590
+ expires_at: util/* default */.A.Date.now() + 7 * 24 * 60 * 60 * 1000
6591
+ });
6592
+ } catch (_) {}
6593
+ return result;
6594
+ }
6595
+ getFormStatus(formid) {
6596
+ const key = websiteFormStorageKey(this.api, formid);
6597
+ try {
6598
+ const receipt = this.kv.match(key);
6599
+ if (!receipt) return {
6600
+ submitted: false,
6601
+ status: 'not_found'
6602
+ };
6603
+ if (!Number.isFinite(receipt.expires_at) || typeof receipt.conversation_id !== 'string' || !receipt.conversation_id) {
6604
+ this.kv.del(key);
6605
+ return {
6606
+ submitted: false,
6607
+ status: 'not_found'
6608
+ };
6609
+ }
6610
+ if (receipt.expires_at <= util/* default */.A.Date.now()) {
6611
+ this.kv.del(key);
6612
+ return {
6613
+ submitted: false,
6614
+ status: 'expired'
6615
+ };
6616
+ }
6617
+ return {
6618
+ submitted: true,
6619
+ status: 'found',
6620
+ conversation_id: receipt.conversation_id
6621
+ };
6622
+ } catch (_) {
6623
+ return {
6624
+ submitted: false,
6625
+ status: 'storage_unavailable'
6626
+ };
6627
+ }
6628
+ }
6580
6629
  async submitForm(formid, data, user) {
6581
6630
  await this.readyGate.entry();
6582
6631
  if (user) await this.setUser(user);
@@ -6704,6 +6753,7 @@ class Store {
6704
6753
  async _onWs(ev) {
6705
6754
  if (this._die) return;
6706
6755
  if (!ev || !ev.type) return;
6756
+ if (util/* default */.A.get(ev, 'touchpoint.channel') === 'form' || util/* default */.A.get(ev, 'data.conversation.touchpoint.channel') === 'form') return;
6707
6757
  ev.created = util/* default */.A.getMs(ev.created);
6708
6758
  if (ev.type === 'user_info_updated') {
6709
6759
  return;
@@ -7459,6 +7509,22 @@ var xhrsend = function (method, url, body, cb) {
7459
7509
  request.send(body);
7460
7510
  };
7461
7511
  const dieErr = new Error('sdk unloaded');
7512
+ function websiteFormStorageKey(api, formid) {
7513
+ return 'sbz_form_submitted:' + util/* default */.A.stringify([api.accid, api.uid, formid]);
7514
+ }
7515
+ function formResult(response) {
7516
+ const body = response && response.body;
7517
+ const detail = response && response.error || body && body.error;
7518
+ const status = response && response.code;
7519
+ const valid = body && typeof body.conversation_id === 'string' && body.conversation_id && typeof body.submission_id === 'string' && body.submission_id;
7520
+ if (detail || !(status >= 200 && status < 300) || !valid) {
7521
+ const error = new Error('Subiz form request failed');
7522
+ error.code = detail && detail.code || 'form_request_failed';
7523
+ error.status = status;
7524
+ throw error;
7525
+ }
7526
+ return body;
7527
+ }
7462
7528
  // EXTERNAL MODULE: ./src/common.js
7463
7529
  var common = __webpack_require__(2840);
7464
7530
  // EXTERNAL MODULE: ./node_modules/preact/dist/preact.module.js
@@ -14149,8 +14215,9 @@ class lib_Widget {
14149
14215
  this.impressionTokens[formid] = impressiontoken;
14150
14216
  return impressiontoken;
14151
14217
  }
14152
- async submitForm(formid, values) {
14153
- if (!formid || !values) return;
14218
+ async submitForm(formid, values, options = {}) {
14219
+ if (!formid || !values) throw new TypeError('submitForm requires a form ID and fields');
14220
+ await this.store.readyGate.entry();
14154
14221
  if (util/* default */.A.startsWith(formid, 'cp')) {
14155
14222
  let storeuser = util/* default */.A.cloneDeep(this.store.matchUser());
14156
14223
  let user = {
@@ -14181,33 +14248,13 @@ class lib_Widget {
14181
14248
  this.store.setUserDiff(user);
14182
14249
  return this.store.sendConversion(formid, values, impressiontoken);
14183
14250
  }
14184
- values = util/* default */.A.filter(values, field => {
14185
- return field.key && field.value;
14186
- });
14187
- let text = 'Khách điền form\n';
14188
- util/* default */.A.map(values, field => {
14189
- text += '+ ' + field.key + ': ' + field.value + '\n';
14190
- });
14191
- return this.store.startConvo({
14192
- subject: 'Khách điền form',
14193
- touchpoint: {
14194
- channel: 'form',
14195
- source: window.location.hostname,
14196
- id: Date.now() + '_' + util/* default */.A.randomString()
14197
- },
14198
- form_submission: {
14199
- id: formid,
14200
- values: values
14201
- },
14202
- message_sent: {
14203
- data: {
14204
- message: {
14205
- format: 'plaintext',
14206
- text: text
14207
- }
14208
- }
14209
- }
14210
- });
14251
+ const payload = formRequest(formid, values, options);
14252
+ return this.store.submitWebsiteForm(formid, payload);
14253
+ }
14254
+ async getFormStatus(formid) {
14255
+ validateWebsiteFormId(formid);
14256
+ await this.store.readyGate.entry();
14257
+ return this.store.getFormStatus(formid);
14211
14258
  }
14212
14259
  startConvo(req) {
14213
14260
  req = util/* default */.A.cloneDeep(req) || {};
@@ -14320,6 +14367,34 @@ class lib_Widget {
14320
14367
  }
14321
14368
  window.__sbz_WIDGET = lib_Widget;
14322
14369
  /* harmony default export */ const lib = (lib_Widget);
14370
+ function validateWebsiteFormId(formid) {
14371
+ if (typeof formid !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(formid)) {
14372
+ throw new TypeError('Form ID must contain 1-64 letters, digits, underscores or hyphens');
14373
+ }
14374
+ if (formid.startsWith('cp')) throw new TypeError('Form IDs starting with cp are reserved for Subiz campaigns');
14375
+ }
14376
+ function formRequest(formid, fields, options = {}) {
14377
+ validateWebsiteFormId(formid);
14378
+ if (!Array.isArray(fields) || !fields.length) throw new TypeError('Form fields must be a nonempty array');
14379
+ if (!options || typeof options !== 'object' || Array.isArray(options)) throw new TypeError('Form options must be an object');
14380
+ return {
14381
+ form_name: options.name === undefined ? formid : options.name,
14382
+ page_url: options.page_url === undefined ? window.location.href : options.page_url,
14383
+ submission_id: options.submission_id === undefined ? util/* default */.A.randomString(36) : options.submission_id,
14384
+ values: fields.map(field => {
14385
+ if (!field || typeof field.key !== 'string') throw new TypeError('Each field requires a key');
14386
+ const value = field.value;
14387
+ if (value != null && !['string', 'number', 'boolean'].includes(typeof value)) {
14388
+ throw new TypeError('Form field values must be strings, numbers or booleans');
14389
+ }
14390
+ return {
14391
+ key: field.key,
14392
+ label: field.label === undefined ? field.key : field.label,
14393
+ value: value == null ? '' : String(value)
14394
+ };
14395
+ })
14396
+ };
14397
+ }
14323
14398
  ;// ./screenshoot.js
14324
14399
 
14325
14400
  let accid = 'acqsulrowbxiugvginhw';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@subiz/widget-v4",
3
- "version": "4.1.213",
3
+ "version": "4.1.215",
4
4
  "main": "./compiled_src/lib.js",
5
5
  "scripts": {
6
6
  "dev": "npx webpack serve --config webpack.dev.js",
package/src/lib.js CHANGED
@@ -208,8 +208,9 @@ class Widget {
208
208
  }
209
209
 
210
210
  // cpxdf, [{key:'fullname', type:'text', value: 'thanhpk@liec.om'}]
211
- async submitForm(formid, values) {
212
- if (!formid || !values) return
211
+ async submitForm(formid, values, options = {}) {
212
+ if (!formid || !values) throw new TypeError('submitForm requires a form ID and fields')
213
+ await this.store.readyGate.entry()
213
214
  if (sb.startsWith(formid, 'cp')) {
214
215
  let storeuser = sb.cloneDeep(this.store.matchUser())
215
216
  let user = {id: storeuser.id, account_id: storeuser.account_id}
@@ -233,28 +234,14 @@ class Widget {
233
234
  return this.store.sendConversion(formid, values, impressiontoken)
234
235
  }
235
236
 
236
- values = sb.filter(values, (field) => {
237
- return field.key && field.value
238
- })
239
- let text = 'Khách điền form\n'
240
- sb.map(values, (field) => {
241
- text += '+ ' + field.key + ': ' + field.value + '\n'
242
- })
243
-
244
- return this.store.startConvo({
245
- subject: 'Khách điền form',
246
- touchpoint: {
247
- channel: 'form',
248
- source: window.location.hostname,
249
- id: Date.now() + '_' + sb.randomString(),
250
- },
251
- form_submission: {id: formid, values: values},
252
- message_sent: {
253
- data: {
254
- message: {format: 'plaintext', text: text},
255
- },
256
- },
257
- })
237
+ const payload = formRequest(formid, values, options)
238
+ return this.store.submitWebsiteForm(formid, payload)
239
+ }
240
+
241
+ async getFormStatus(formid) {
242
+ validateWebsiteFormId(formid)
243
+ await this.store.readyGate.entry()
244
+ return this.store.getFormStatus(formid)
258
245
  }
259
246
 
260
247
  // req.bot_id != "" -> start conversation with bot id
@@ -404,3 +391,30 @@ window.__sbz_WIDGET = Widget
404
391
  export default Widget
405
392
 
406
393
  // lib.submitForm('cprkztryyobndhbvwzjab', [{key: 'fullname', value: 'Pham Kieu Thanh 2' }, {key: 'phones', value: "0366621895"}]
394
+
395
+
396
+ function validateWebsiteFormId(formid) {
397
+ if (typeof formid !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(formid)) {
398
+ throw new TypeError('Form ID must contain 1-64 letters, digits, underscores or hyphens')
399
+ }
400
+ if (formid.startsWith('cp')) throw new TypeError('Form IDs starting with cp are reserved for Subiz campaigns')
401
+ }
402
+
403
+ function formRequest(formid, fields, options = {}) {
404
+ validateWebsiteFormId(formid)
405
+ if (!Array.isArray(fields) || !fields.length) throw new TypeError('Form fields must be a nonempty array')
406
+ if (!options || typeof options !== 'object' || Array.isArray(options)) throw new TypeError('Form options must be an object')
407
+ return {
408
+ form_name: options.name === undefined ? formid : options.name,
409
+ page_url: options.page_url === undefined ? window.location.href : options.page_url,
410
+ submission_id: options.submission_id === undefined ? sb.randomString(36) : options.submission_id,
411
+ values: fields.map((field) => {
412
+ if (!field || typeof field.key !== 'string') throw new TypeError('Each field requires a key')
413
+ const value = field.value
414
+ if (value != null && !['string', 'number', 'boolean'].includes(typeof value)) {
415
+ throw new TypeError('Form field values must be strings, numbers or booleans')
416
+ }
417
+ return {key: field.key, label: field.label === undefined ? field.key : field.label, value: value == null ? '' : String(value)}
418
+ }),
419
+ }
420
+ }
package/src/store/api.js CHANGED
@@ -271,6 +271,12 @@ export default class {
271
271
  .addQuery('x-referer', sb.get(location, 'origin', ''))
272
272
  .post(`/accounts/${this.accid}/users/${this.uid}/events`, data)
273
273
 
274
+ // Keep the configured API host and all widget authentication hooks.
275
+ websiteFormRequest = () => this.apireq.setBaseUrl(this.ApiURL.replace(/\/4\.[01]\/?$/, '/4.1'))
276
+
277
+ submitWebsiteForm = (formid, payload) => this.websiteFormRequest()
278
+ .post(`/accounts/${this.accid}/external-forms/${encodeURIComponent(formid)}/submissions`, payload)
279
+
274
280
  submitForm = (form_id, page_url, device, data, user) =>
275
281
  this.apireq
276
282
  .addQuery('x-referer', sb.get(location, 'origin', ''))
@@ -955,6 +955,36 @@ export default class Store {
955
955
  }
956
956
  }
957
957
 
958
+ async submitWebsiteForm(formid, payload) {
959
+ await this.readyGate.entry()
960
+ const key = websiteFormStorageKey(this.api, formid)
961
+ const kv = this.kv
962
+ const result = formResult(await this.api.submitWebsiteForm(formid, payload))
963
+ try {
964
+ kv.put(key, {conversation_id: result.conversation_id, expires_at: sb.Date.now() + 7 * 24 * 60 * 60 * 1000})
965
+ } catch (_) { /* Storage failure must not make callers retry a successful submission. */ }
966
+ return result
967
+ }
968
+
969
+ getFormStatus(formid) {
970
+ const key = websiteFormStorageKey(this.api, formid)
971
+ try {
972
+ const receipt = this.kv.match(key)
973
+ if (!receipt) return {submitted: false, status: 'not_found'}
974
+ if (!Number.isFinite(receipt.expires_at) || typeof receipt.conversation_id !== 'string' || !receipt.conversation_id) {
975
+ this.kv.del(key)
976
+ return {submitted: false, status: 'not_found'}
977
+ }
978
+ if (receipt.expires_at <= sb.Date.now()) {
979
+ this.kv.del(key)
980
+ return {submitted: false, status: 'expired'}
981
+ }
982
+ return {submitted: true, status: 'found', conversation_id: receipt.conversation_id}
983
+ } catch (_) {
984
+ return {submitted: false, status: 'storage_unavailable'}
985
+ }
986
+ }
987
+
958
988
  async submitForm(formid, data, user) {
959
989
  await this.readyGate.entry()
960
990
  if (user) await this.setUser(user)
@@ -1117,6 +1147,9 @@ export default class Store {
1117
1147
  async _onWs(ev) {
1118
1148
  if (this._die) return
1119
1149
  if (!ev || !ev.type) return
1150
+ // Website form conversations are handled in the agent inbox, not the chat widget.
1151
+ // Ignore their realtime events before an unknown conversation triggers a GET.
1152
+ if (sb.get(ev, 'touchpoint.channel') === 'form' || sb.get(ev, 'data.conversation.touchpoint.channel') === 'form') return
1120
1153
  ev.created = sb.getMs(ev.created)
1121
1154
  if (ev.type === 'user_info_updated') {
1122
1155
  return
@@ -1916,3 +1949,21 @@ var xhrsend = function (method, url, body, cb) {
1916
1949
  }
1917
1950
 
1918
1951
  const dieErr = new Error('sdk unloaded')
1952
+
1953
+ function websiteFormStorageKey(api, formid) {
1954
+ return 'sbz_form_submitted:' + sb.stringify([api.accid, api.uid, formid])
1955
+ }
1956
+
1957
+ function formResult(response) {
1958
+ const body = response && response.body
1959
+ const detail = (response && response.error) || (body && body.error)
1960
+ const status = response && response.code
1961
+ const valid = body && typeof body.conversation_id === 'string' && body.conversation_id && typeof body.submission_id === 'string' && body.submission_id
1962
+ if (detail || !(status >= 200 && status < 300) || !valid) {
1963
+ const error = new Error('Subiz form request failed')
1964
+ error.code = (detail && detail.code) || 'form_request_failed'
1965
+ error.status = status
1966
+ throw error
1967
+ }
1968
+ return body
1969
+ }