@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.
- package/README.md +6 -0
- package/api.md +24 -8
- package/assets/img/bubble_default.svg +1 -0
- package/assets/less/widget.less +12 -0
- package/compiled_src/chatbox/ChatButton.js +19 -17
- package/compiled_src/lib.js +2 -15
- package/compiled_src/store/api.js +0 -1
- package/compiled_src/store/store.js +68 -1
- package/dist/{1a12a372880.278.js → 1a12ecfb6db.278.js} +1 -1
- package/dist/{1a12a372880.webview.js → 1a12ecfb6db.app.js} +1 -1
- package/dist/{1a12a372880.chat.html → 1a12ecfb6db.chat.html} +1 -1
- package/dist/1a12ecfb6db.chat.js +1 -0
- package/dist/{1a12a372880.form.html → 1a12ecfb6db.form.html} +1 -1
- package/dist/1a12ecfb6db.form.js +1 -0
- package/dist/{1a12a372880.index.html → 1a12ecfb6db.index.html} +1 -1
- package/dist/{1a12a372880.library.html → 1a12ecfb6db.library.html} +1 -1
- package/dist/1a12ecfb6db.library.js +1 -0
- package/dist/{1a12a372880.preview.html → 1a12ecfb6db.preview.html} +1 -1
- package/dist/1a12ecfb6db.preview.js +1 -0
- package/dist/1a12ecfb6db.webview.js +1 -0
- package/dist/HASH +1 -0
- package/dist/assets/img/{bubble_default.7d5e4ab.svg → bubble_default.6f6eec8.svg} +1 -0
- package/dist/screenshoot.js +90 -35
- package/package.json +1 -1
- package/src/chatbox/ChatButton.js +22 -20
- package/src/lib.js +2 -18
- package/src/store/api.js +0 -3
- package/src/store/store.js +52 -0
- package/up +18 -0
- package/dist/.HASH +0 -1
- package/dist/1a12a372880.app.js +0 -1
- package/dist/1a12a372880.chat.js +0 -1
- package/dist/1a12a372880.form.js +0 -1
- package/dist/1a12a372880.library.js +0 -1
- package/dist/1a12a372880.preview.js +0 -1
- /package/dist/{1a12a372880.127.js → 1a12ecfb6db.127.js} +0 -0
- /package/dist/{1a12a372880.132.js → 1a12ecfb6db.132.js} +0 -0
- /package/dist/{1a12a372880.185.js → 1a12ecfb6db.185.js} +0 -0
- /package/dist/{1a12a372880.27.js → 1a12ecfb6db.27.js} +0 -0
- /package/dist/{1a12a372880.339.js → 1a12ecfb6db.339.js} +0 -0
- /package/dist/{1a12a372880.459.js → 1a12ecfb6db.459.js} +0 -0
- /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)`
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
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.
|
package/assets/less/widget.less
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
632
|
-
|
|
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
|
-
|
|
635
|
-
|
|
635
|
+
return (await preloadButtonImage(src, {
|
|
636
|
+
crossOrigin: true
|
|
637
|
+
})) ? src : null;
|
|
636
638
|
}
|
package/compiled_src/lib.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
+
}
|