@subiz/widget-v4 4.1.212 → 4.1.214

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 (33) hide show
  1. package/api.md +57 -0
  2. package/compiled_src/lib.js +52 -30
  3. package/compiled_src/store/api.js +3 -0
  4. package/compiled_src/store/store.js +16 -0
  5. package/dist/.HASH +1 -0
  6. package/dist/{1a0f6f3ff1a.27.js → 1a12a372880.27.js} +1 -1
  7. package/dist/{1a0f6f3ff1a.339.js → 1a12a372880.339.js} +1 -1
  8. package/dist/{1a0f6f3ff1a.459.js → 1a12a372880.459.js} +1 -1
  9. package/dist/{1a0f6f3ff1a.app.js → 1a12a372880.app.js} +1 -1
  10. package/dist/{1a0f6f3ff1a.chat.html → 1a12a372880.chat.html} +1 -1
  11. package/dist/{1a0f6f3ff1a.chat.js → 1a12a372880.chat.js} +1 -1
  12. package/dist/{1a0f6f3ff1a.form.html → 1a12a372880.form.html} +1 -1
  13. package/dist/1a12a372880.form.js +1 -0
  14. package/dist/{1a0f6f3ff1a.index.html → 1a12a372880.index.html} +1 -1
  15. package/dist/{1a0f6f3ff1a.library.html → 1a12a372880.library.html} +1 -1
  16. package/dist/{1a0f6f3ff1a.library.js → 1a12a372880.library.js} +1 -1
  17. package/dist/{1a0f6f3ff1a.preview.html → 1a12a372880.preview.html} +1 -1
  18. package/dist/{1a0f6f3ff1a.preview.js → 1a12a372880.preview.js} +1 -1
  19. package/dist/{1a0f6f3ff1a.webview.js → 1a12a372880.webview.js} +1 -1
  20. package/dist/27.js +1 -1
  21. package/dist/459.js +1 -1
  22. package/dist/screenshoot.js +70 -29
  23. package/package.json +3 -3
  24. package/src/lib.js +54 -24
  25. package/src/store/api.js +9 -0
  26. package/src/store/store.js +18 -0
  27. package/dist/1a0f6f3ff1a.form.js +0 -1
  28. package/dist/HASH +0 -1
  29. /package/dist/{1a0f6f3ff1a.127.js → 1a12a372880.127.js} +0 -0
  30. /package/dist/{1a0f6f3ff1a.132.js → 1a12a372880.132.js} +0 -0
  31. /package/dist/{1a0f6f3ff1a.185.js → 1a12a372880.185.js} +0 -0
  32. /package/dist/{1a0f6f3ff1a.278.js → 1a12a372880.278.js} +0 -0
  33. /package/dist/{1a0f6f3ff1a.490.js → 1a12a372880.490.js} +0 -0
@@ -4586,6 +4586,9 @@ 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));
4591
+ _defineProperty(this, "getWebsiteFormStatus", formid => this.websiteFormRequest().get(`/accounts/${this.accid}/external-forms/${encodeURIComponent(formid)}/status`));
4589
4592
  _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
4593
  by: {
4591
4594
  id: this.uid,
@@ -5721,6 +5724,9 @@ cookie.defaults.expires = 3650;
5721
5724
  let store_memlocalStorage = (inmem_localstorage_namespaceFn());
5722
5725
  const MAX_CONVO = 20;
5723
5726
  const page_loaded = util/* default */.A.Date.now();
5727
+ const CHAT_STATE_RESET_AFTER_MS = 24 * 60 * 60 * 1000;
5728
+ const CHAT_STATE_CYCLE_STARTED_AT = 'chat_state_cycle_started_at';
5729
+ const RESETTABLE_CHAT_STATE_KEYS = ['lastview', 'light_convos', 'bot_run_logs', 'close_preview_at'];
5724
5730
  class Store {
5725
5731
  constructor(ApiURL, FileURL, RealtimeURL, inmem, fakeapi) {
5726
5732
  store_defineProperty(this, "inmem", false);
@@ -6394,6 +6400,18 @@ class Store {
6394
6400
  }
6395
6401
  this.kv.put('lastview', lastView);
6396
6402
  }
6403
+ resetChatStateIfExpired() {
6404
+ const now = this.now();
6405
+ const cycleStartedAt = this.kv.match(CHAT_STATE_CYCLE_STARTED_AT);
6406
+ if (!Number.isFinite(cycleStartedAt) || cycleStartedAt <= 0 || cycleStartedAt > now) {
6407
+ this.kv.put(CHAT_STATE_CYCLE_STARTED_AT, now);
6408
+ return false;
6409
+ }
6410
+ if (now - cycleStartedAt < CHAT_STATE_RESET_AFTER_MS) return false;
6411
+ util/* default */.A.map(RESETTABLE_CHAT_STATE_KEYS, key => this.kv.del(key));
6412
+ this.kv.put(CHAT_STATE_CYCLE_STARTED_AT, now);
6413
+ return true;
6414
+ }
6397
6415
  async _initResource(accid, auth, realtimep) {
6398
6416
  this.g_sync = {};
6399
6417
  let user = await this.api.setAuth(auth);
@@ -6404,6 +6422,7 @@ class Store {
6404
6422
  }
6405
6423
  this.readyGate.close();
6406
6424
  accid = (accid || '').trim();
6425
+ this.resetChatStateIfExpired();
6407
6426
  let lightConvos = this.kv.match('light_convos') || {};
6408
6427
  this.lightConvos = {};
6409
6428
  util/* default */.A.map(lightConvos, convo => {
@@ -14133,8 +14152,9 @@ class lib_Widget {
14133
14152
  this.impressionTokens[formid] = impressiontoken;
14134
14153
  return impressiontoken;
14135
14154
  }
14136
- async submitForm(formid, values) {
14137
- if (!formid || !values) return;
14155
+ async submitForm(formid, values, options = {}) {
14156
+ if (!formid || !values) throw new TypeError('submitForm requires a form ID and fields');
14157
+ await this.store.readyGate.entry();
14138
14158
  if (util/* default */.A.startsWith(formid, 'cp')) {
14139
14159
  let storeuser = util/* default */.A.cloneDeep(this.store.matchUser());
14140
14160
  let user = {
@@ -14165,33 +14185,13 @@ class lib_Widget {
14165
14185
  this.store.setUserDiff(user);
14166
14186
  return this.store.sendConversion(formid, values, impressiontoken);
14167
14187
  }
14168
- values = util/* default */.A.filter(values, field => {
14169
- return field.key && field.value;
14170
- });
14171
- let text = 'Khách điền form\n';
14172
- util/* default */.A.map(values, field => {
14173
- text += '+ ' + field.key + ': ' + field.value + '\n';
14174
- });
14175
- return this.store.startConvo({
14176
- subject: 'Khách điền form',
14177
- touchpoint: {
14178
- channel: 'form',
14179
- source: window.location.hostname,
14180
- id: Date.now() + '_' + util/* default */.A.randomString()
14181
- },
14182
- form_submission: {
14183
- id: formid,
14184
- values: values
14185
- },
14186
- message_sent: {
14187
- data: {
14188
- message: {
14189
- format: 'plaintext',
14190
- text: text
14191
- }
14192
- }
14193
- }
14194
- });
14188
+ const payload = formRequest(formid, values, options);
14189
+ return formResult(await this.store.api.submitWebsiteForm(formid, payload), 'submit');
14190
+ }
14191
+ async getFormStatus(formid) {
14192
+ validateWebsiteFormId(formid);
14193
+ await this.store.readyGate.entry();
14194
+ return formResult(await this.store.api.getWebsiteFormStatus(formid), 'status');
14195
14195
  }
14196
14196
  startConvo(req) {
14197
14197
  req = util/* default */.A.cloneDeep(req) || {};
@@ -14304,6 +14304,47 @@ class lib_Widget {
14304
14304
  }
14305
14305
  window.__sbz_WIDGET = lib_Widget;
14306
14306
  /* harmony default export */ const lib = (lib_Widget);
14307
+ function validateWebsiteFormId(formid) {
14308
+ if (typeof formid !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(formid)) {
14309
+ throw new TypeError('Form ID must contain 1-64 letters, digits, underscores or hyphens');
14310
+ }
14311
+ if (formid.startsWith('cp')) throw new TypeError('Form IDs starting with cp are reserved for Subiz campaigns');
14312
+ }
14313
+ function formRequest(formid, fields, options = {}) {
14314
+ validateWebsiteFormId(formid);
14315
+ if (!Array.isArray(fields) || !fields.length) throw new TypeError('Form fields must be a nonempty array');
14316
+ if (!options || typeof options !== 'object' || Array.isArray(options)) throw new TypeError('Form options must be an object');
14317
+ return {
14318
+ form_name: options.name === undefined ? formid : options.name,
14319
+ page_url: options.page_url === undefined ? window.location.href : options.page_url,
14320
+ submission_id: options.submission_id === undefined ? util/* default */.A.randomString(36) : options.submission_id,
14321
+ values: fields.map(field => {
14322
+ if (!field || typeof field.key !== 'string') throw new TypeError('Each field requires a key');
14323
+ const value = field.value;
14324
+ if (value != null && !['string', 'number', 'boolean'].includes(typeof value)) {
14325
+ throw new TypeError('Form field values must be strings, numbers or booleans');
14326
+ }
14327
+ return {
14328
+ key: field.key,
14329
+ label: field.label === undefined ? field.key : field.label,
14330
+ value: value == null ? '' : String(value)
14331
+ };
14332
+ })
14333
+ };
14334
+ }
14335
+ function formResult(response, operation) {
14336
+ const body = response && response.body;
14337
+ const detail = response && response.error || body && body.error;
14338
+ const status = response && response.code;
14339
+ 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';
14340
+ if (detail || !(status >= 200 && status < 300) || !valid) {
14341
+ const error = new Error('Subiz form request failed');
14342
+ error.code = detail && detail.code || 'form_request_failed';
14343
+ error.status = status;
14344
+ throw error;
14345
+ }
14346
+ return body;
14347
+ }
14307
14348
  ;// ./screenshoot.js
14308
14349
 
14309
14350
  let accid = 'acqsulrowbxiugvginhw';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@subiz/widget-v4",
3
- "version": "4.1.212",
3
+ "version": "4.1.214",
4
4
  "main": "./compiled_src/lib.js",
5
5
  "scripts": {
6
6
  "dev": "npx webpack serve --config webpack.dev.js",
@@ -32,10 +32,10 @@
32
32
  "@babel/preset-env": "^7.29.7",
33
33
  "babel-loader": "^10.1.1",
34
34
  "babel-plugin-syntax-jsx": "^6.18.0",
35
- "eslint": "^10.11.0",
35
+ "eslint": "^10.12.0",
36
36
  "globals": "^17.13.0",
37
37
  "html-webpack-plugin": "^5.6.8",
38
- "jsdom": "^30.1.1",
38
+ "jsdom": "^30.1.2",
39
39
  "puppeteer": "^25.12.0",
40
40
  "tape": "^5.10.2",
41
41
  "webpack": "^5.111.1",
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 formResult(await this.store.api.submitWebsiteForm(formid, payload), 'submit')
239
+ }
240
+
241
+ async getFormStatus(formid) {
242
+ validateWebsiteFormId(formid)
243
+ await this.store.readyGate.entry()
244
+ return formResult(await this.store.api.getWebsiteFormStatus(formid), 'status')
258
245
  }
259
246
 
260
247
  // req.bot_id != "" -> start conversation with bot id
@@ -404,3 +391,46 @@ 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
+ }
421
+
422
+ function formResult(response, operation) {
423
+ const body = response && response.body
424
+ const detail = (response && response.error) || (body && body.error)
425
+ const status = response && response.code
426
+ const valid = operation === 'submit'
427
+ ? body && typeof body.conversation_id === 'string' && body.conversation_id && typeof body.submission_id === 'string' && body.submission_id
428
+ : body && typeof body.submitted === 'boolean' && typeof body.status === 'string'
429
+ if (detail || !(status >= 200 && status < 300) || !valid) {
430
+ const error = new Error('Subiz form request failed')
431
+ error.code = (detail && detail.code) || 'form_request_failed'
432
+ error.status = status
433
+ throw error
434
+ }
435
+ return body
436
+ }
package/src/store/api.js CHANGED
@@ -271,6 +271,15 @@ 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
+
280
+ getWebsiteFormStatus = (formid) => this.websiteFormRequest()
281
+ .get(`/accounts/${this.accid}/external-forms/${encodeURIComponent(formid)}/status`)
282
+
274
283
  submitForm = (form_id, page_url, device, data, user) =>
275
284
  this.apireq
276
285
  .addQuery('x-referer', sb.get(location, 'origin', ''))
@@ -15,6 +15,9 @@ cookie.defaults.expires = 3650 // 10 year
15
15
  let memlocalStorage = require('./inmem_localstorage.js')
16
16
  const MAX_CONVO = 20
17
17
  const page_loaded = sb.Date.now()
18
+ const CHAT_STATE_RESET_AFTER_MS = 24 * 60 * 60 * 1000
19
+ const CHAT_STATE_CYCLE_STARTED_AT = 'chat_state_cycle_started_at'
20
+ const RESETTABLE_CHAT_STATE_KEYS = ['lastview', 'light_convos', 'bot_run_logs', 'close_preview_at']
18
21
 
19
22
  // available localstorage key mask, user, hasManuallyClosed, isopen, close_preview_at, lastview, bot_run_logs, plugin_run_logs
20
23
  export default class Store {
@@ -740,6 +743,20 @@ export default class Store {
740
743
  this.kv.put('lastview', lastView)
741
744
  }
742
745
 
746
+ resetChatStateIfExpired() {
747
+ const now = this.now()
748
+ const cycleStartedAt = this.kv.match(CHAT_STATE_CYCLE_STARTED_AT)
749
+ if (!Number.isFinite(cycleStartedAt) || cycleStartedAt <= 0 || cycleStartedAt > now) {
750
+ this.kv.put(CHAT_STATE_CYCLE_STARTED_AT, now)
751
+ return false
752
+ }
753
+ if (now - cycleStartedAt < CHAT_STATE_RESET_AFTER_MS) return false
754
+
755
+ sb.map(RESETTABLE_CHAT_STATE_KEYS, (key) => this.kv.del(key))
756
+ this.kv.put(CHAT_STATE_CYCLE_STARTED_AT, now)
757
+ return true
758
+ }
759
+
743
760
  async _initResource(accid, auth, realtimep) {
744
761
  this.g_sync = {}
745
762
 
@@ -752,6 +769,7 @@ export default class Store {
752
769
 
753
770
  this.readyGate.close()
754
771
  accid = (accid || '').trim()
772
+ this.resetChatStateIfExpired()
755
773
  let lightConvos = this.kv.match('light_convos') || {}
756
774
  this.lightConvos = {}
757
775
  sb.map(lightConvos, (convo) => {