Changeset 3435658
- Timestamp:
- 01/09/2026 06:49:22 AM (3 months ago)
- Location:
- bettercx-widget
- Files:
-
- 214 added
- 12 edited
-
tags/1.0.19 (added)
-
tags/1.0.19/assets (added)
-
tags/1.0.19/assets/admin.css (added)
-
tags/1.0.19/assets/admin.js (added)
-
tags/1.0.19/assets/bettercx-widget.css (added)
-
tags/1.0.19/assets/bettercx-widget.esm.js (added)
-
tags/1.0.19/assets/bettercx-widget.js (added)
-
tags/1.0.19/assets/index.esm.js (added)
-
tags/1.0.19/assets/p-0018ac87.entry.js (added)
-
tags/1.0.19/assets/p-00a672a6.js (added)
-
tags/1.0.19/assets/p-0b0acabd.entry.js (added)
-
tags/1.0.19/assets/p-0c760e7b.entry.js (added)
-
tags/1.0.19/assets/p-150b7110.entry.js (added)
-
tags/1.0.19/assets/p-1874a1df.entry.js (added)
-
tags/1.0.19/assets/p-20b914cf.system.entry.js (added)
-
tags/1.0.19/assets/p-2248a9bc.entry.js (added)
-
tags/1.0.19/assets/p-295a94dc.system.entry.js (added)
-
tags/1.0.19/assets/p-2b3ed780.system.entry.js (added)
-
tags/1.0.19/assets/p-2b7d273b.entry.js (added)
-
tags/1.0.19/assets/p-2uyAWbl7.js (added)
-
tags/1.0.19/assets/p-3056f770.system.entry.js (added)
-
tags/1.0.19/assets/p-31a11738.system.entry.js (added)
-
tags/1.0.19/assets/p-339847ab.entry.js (added)
-
tags/1.0.19/assets/p-3XnA6jB5.system.js (added)
-
tags/1.0.19/assets/p-3b907399.system.entry.js (added)
-
tags/1.0.19/assets/p-3ba50f50.js (added)
-
tags/1.0.19/assets/p-3c13f4d3.js (added)
-
tags/1.0.19/assets/p-3d3a14d1.js (added)
-
tags/1.0.19/assets/p-43f268bb.entry.js (added)
-
tags/1.0.19/assets/p-4a118785.entry.js (added)
-
tags/1.0.19/assets/p-4de43b13.entry.js (added)
-
tags/1.0.19/assets/p-53dddee8.entry.js (added)
-
tags/1.0.19/assets/p-553fc434.entry.js (added)
-
tags/1.0.19/assets/p-5826ffe8.system.entry.js (added)
-
tags/1.0.19/assets/p-5895aae6.js (added)
-
tags/1.0.19/assets/p-5a697cc1.entry.js (added)
-
tags/1.0.19/assets/p-5d4e27c5.system.entry.js (added)
-
tags/1.0.19/assets/p-60f346f7.entry.js (added)
-
tags/1.0.19/assets/p-65e11e89.entry.js (added)
-
tags/1.0.19/assets/p-65f90db1.system.entry.js (added)
-
tags/1.0.19/assets/p-6885dee3.entry.js (added)
-
tags/1.0.19/assets/p-6nBrNW2K.system.js (added)
-
tags/1.0.19/assets/p-72aa52b9.entry.js (added)
-
tags/1.0.19/assets/p-73aa3697.entry.js (added)
-
tags/1.0.19/assets/p-77276af2.js (added)
-
tags/1.0.19/assets/p-7e2075e2.system.entry.js (added)
-
tags/1.0.19/assets/p-887c5563.system.entry.js (added)
-
tags/1.0.19/assets/p-8cab92ea.system.entry.js (added)
-
tags/1.0.19/assets/p-9581ee13.system.entry.js (added)
-
tags/1.0.19/assets/p-9642a595.entry.js (added)
-
tags/1.0.19/assets/p-9a1531b6.entry.js (added)
-
tags/1.0.19/assets/p-9e49a311.css (added)
-
tags/1.0.19/assets/p-B-UbqJtt.system.js (added)
-
tags/1.0.19/assets/p-B1i_i5T1.system.js (added)
-
tags/1.0.19/assets/p-B2qTTSQX.system.js (added)
-
tags/1.0.19/assets/p-B6-Pa1f9.js (added)
-
tags/1.0.19/assets/p-B7XTg7r_.system.js (added)
-
tags/1.0.19/assets/p-B8xTUJN9.system.js (added)
-
tags/1.0.19/assets/p-BAn8W8NT.js (added)
-
tags/1.0.19/assets/p-BTuH8KN8.system.js (added)
-
tags/1.0.19/assets/p-BTuzHDoC.js (added)
-
tags/1.0.19/assets/p-BU1bGO0l.js (added)
-
tags/1.0.19/assets/p-BXgDY9sK.system.js (added)
-
tags/1.0.19/assets/p-BZjga0WH.system.js (added)
-
tags/1.0.19/assets/p-B_-5Lr6R.system.js (added)
-
tags/1.0.19/assets/p-Be7T8ATp.js (added)
-
tags/1.0.19/assets/p-BjN8JUpv.js (added)
-
tags/1.0.19/assets/p-BkPK7wz1.system.js (added)
-
tags/1.0.19/assets/p-Bm5fqHLr.js (added)
-
tags/1.0.19/assets/p-BnsX22WT.js (added)
-
tags/1.0.19/assets/p-BpuiGdNn.js (added)
-
tags/1.0.19/assets/p-BxBqyhWA.system.js (added)
-
tags/1.0.19/assets/p-BzbNFKTd.system.js (added)
-
tags/1.0.19/assets/p-C3gfgiHm.system.js (added)
-
tags/1.0.19/assets/p-CEVyiIZL.system.js (added)
-
tags/1.0.19/assets/p-CH5038gf.js (added)
-
tags/1.0.19/assets/p-CMpJ07-r.system.js (added)
-
tags/1.0.19/assets/p-CNYKTDM5.js (added)
-
tags/1.0.19/assets/p-CPIXO-ae.system.js (added)
-
tags/1.0.19/assets/p-CVMudTEk.js (added)
-
tags/1.0.19/assets/p-C_NDxfuU.js (added)
-
tags/1.0.19/assets/p-CaA-qsLo.system.js (added)
-
tags/1.0.19/assets/p-Cbgoi924.system.js (added)
-
tags/1.0.19/assets/p-CeQEpW8i.system.js (added)
-
tags/1.0.19/assets/p-ClU3hFGo.system.js (added)
-
tags/1.0.19/assets/p-CnEG-4Gn.system.js (added)
-
tags/1.0.19/assets/p-CnS1xje7.system.js (added)
-
tags/1.0.19/assets/p-CnZyNQad.system.js (added)
-
tags/1.0.19/assets/p-CvsvHMzI.system.js (added)
-
tags/1.0.19/assets/p-D1f_KIu9.system.js (added)
-
tags/1.0.19/assets/p-D6uJovtX.js (added)
-
tags/1.0.19/assets/p-DHFXmR1e.system.js (added)
-
tags/1.0.19/assets/p-DHSoJoxm.system.js (added)
-
tags/1.0.19/assets/p-DKm-2v3i.system.js (added)
-
tags/1.0.19/assets/p-DOTTKS3U.js (added)
-
tags/1.0.19/assets/p-DP4_DZVe.system.js (added)
-
tags/1.0.19/assets/p-DP9ZUtAa.js (added)
-
tags/1.0.19/assets/p-DUC3FSL1.js (added)
-
tags/1.0.19/assets/p-DbcF2T4N.js (added)
-
tags/1.0.19/assets/p-DdHK2Ifq.js (added)
-
tags/1.0.19/assets/p-DpPdwT08.system.js (added)
-
tags/1.0.19/assets/p-DtrG8QEa.system.js (added)
-
tags/1.0.19/assets/p-Dv8OCppZ.system.js (added)
-
tags/1.0.19/assets/p-Fmh_QAnl.system.js (added)
-
tags/1.0.19/assets/p-I2OgTEnz.js (added)
-
tags/1.0.19/assets/p-KzFxM0_g.system.js (added)
-
tags/1.0.19/assets/p-MpQIPKay.js (added)
-
tags/1.0.19/assets/p-N-gVfPlF.system.js (added)
-
tags/1.0.19/assets/p-V8up-zPo.system.js (added)
-
tags/1.0.19/assets/p-Y8RLf6Nz.system.js (added)
-
tags/1.0.19/assets/p-a6ca0260.entry.js (added)
-
tags/1.0.19/assets/p-a8a1179f.system.entry.js (added)
-
tags/1.0.19/assets/p-b1c0a8b9.entry.js (added)
-
tags/1.0.19/assets/p-b4d3d8fa.js (added)
-
tags/1.0.19/assets/p-b74ecaef.system.entry.js (added)
-
tags/1.0.19/assets/p-b7953413.system.entry.js (added)
-
tags/1.0.19/assets/p-b99de9e5.system.entry.js (added)
-
tags/1.0.19/assets/p-ba1adebc.system.entry.js (added)
-
tags/1.0.19/assets/p-bbbb0d6a.system.entry.js (added)
-
tags/1.0.19/assets/p-bcfS9773.system.js (added)
-
tags/1.0.19/assets/p-be67a142.system.entry.js (added)
-
tags/1.0.19/assets/p-bf358854.js (added)
-
tags/1.0.19/assets/p-bf4d2555.system.entry.js (added)
-
tags/1.0.19/assets/p-c369a3e0.system.entry.js (added)
-
tags/1.0.19/assets/p-c86a2b91.system.entry.js (added)
-
tags/1.0.19/assets/p-c8e1015d.js (added)
-
tags/1.0.19/assets/p-cf8b0458.system.entry.js (added)
-
tags/1.0.19/assets/p-cfd83235.entry.js (added)
-
tags/1.0.19/assets/p-d2158100.js (added)
-
tags/1.0.19/assets/p-d2ce937e.js (added)
-
tags/1.0.19/assets/p-d434cc1f.system.entry.js (added)
-
tags/1.0.19/assets/p-d51d9897.entry.js (added)
-
tags/1.0.19/assets/p-d7afb562.system.entry.js (added)
-
tags/1.0.19/assets/p-deee3edb.js (added)
-
tags/1.0.19/assets/p-e1740149.system.entry.js (added)
-
tags/1.0.19/assets/p-e913b590.entry.js (added)
-
tags/1.0.19/assets/p-eV7FkxIV.system.js (added)
-
tags/1.0.19/assets/p-eb97a27d.entry.js (added)
-
tags/1.0.19/assets/p-ef0a694e.system.entry.js (added)
-
tags/1.0.19/assets/p-f65647a8.entry.js (added)
-
tags/1.0.19/assets/p-fbee627a.entry.js (added)
-
tags/1.0.19/assets/p-fc52ba98.system.entry.js (added)
-
tags/1.0.19/assets/p-h9j_EzIm.system.js (added)
-
tags/1.0.19/assets/p-l0Y_uHXd.js (added)
-
tags/1.0.19/assets/p-rDa_E45R.system.js (added)
-
tags/1.0.19/assets/p-uHCEnLBu.system.js (added)
-
tags/1.0.19/assets/p-usCtAouH.js (added)
-
tags/1.0.19/assets/p-w4c8J7H6.system.js (added)
-
tags/1.0.19/assets/p-wFIZQlYl.system.js (added)
-
tags/1.0.19/assets/p-zbQHIKau.system.js (added)
-
tags/1.0.19/bettercx-widget.php (added)
-
tags/1.0.19/index.php (added)
-
tags/1.0.19/languages (added)
-
tags/1.0.19/languages/bettercx-widget.pot (added)
-
tags/1.0.19/languages/index.php (added)
-
tags/1.0.19/readme.txt (added)
-
tags/1.0.19/src (added)
-
tags/1.0.19/src/components (added)
-
tags/1.0.19/src/components.d.ts (added)
-
tags/1.0.19/src/components/bcx-chat-list (added)
-
tags/1.0.19/src/components/bcx-chat-list/__tests__ (added)
-
tags/1.0.19/src/components/bcx-chat-list/__tests__/bcx-chat-list.spec.ts (added)
-
tags/1.0.19/src/components/bcx-chat-list/bcx-chat-list.scss (added)
-
tags/1.0.19/src/components/bcx-chat-list/bcx-chat-list.tsx (added)
-
tags/1.0.19/src/components/bcx-chat-list/readme.md (added)
-
tags/1.0.19/src/components/bcx-message-composer (added)
-
tags/1.0.19/src/components/bcx-message-composer/__tests__ (added)
-
tags/1.0.19/src/components/bcx-message-composer/__tests__/bcx-message-composer.spec.ts (added)
-
tags/1.0.19/src/components/bcx-message-composer/bcx-message-composer.scss (added)
-
tags/1.0.19/src/components/bcx-message-composer/bcx-message-composer.tsx (added)
-
tags/1.0.19/src/components/bcx-message-composer/readme.md (added)
-
tags/1.0.19/src/components/bcx-product-slider (added)
-
tags/1.0.19/src/components/bcx-product-slider/__tests__ (added)
-
tags/1.0.19/src/components/bcx-product-slider/__tests__/bcx-product-slider.spec.ts (added)
-
tags/1.0.19/src/components/bcx-product-slider/bcx-product-slider.scss (added)
-
tags/1.0.19/src/components/bcx-product-slider/bcx-product-slider.tsx (added)
-
tags/1.0.19/src/components/bcx-product-slider/readme.md (added)
-
tags/1.0.19/src/components/bettercx-widget (added)
-
tags/1.0.19/src/components/bettercx-widget/__tests__ (added)
-
tags/1.0.19/src/components/bettercx-widget/__tests__/__snapshots__ (added)
-
tags/1.0.19/src/components/bettercx-widget/__tests__/bettercx-widget.e2e.ts (added)
-
tags/1.0.19/src/components/bettercx-widget/__tests__/bettercx-widget.spec.ts (added)
-
tags/1.0.19/src/components/bettercx-widget/bettercx-widget.scss (added)
-
tags/1.0.19/src/components/bettercx-widget/bettercx-widget.tsx (added)
-
tags/1.0.19/src/components/bettercx-widget/readme.md (added)
-
tags/1.0.19/src/global (added)
-
tags/1.0.19/src/global/global.scss (added)
-
tags/1.0.19/src/index.html (added)
-
tags/1.0.19/src/index.ts (added)
-
tags/1.0.19/src/services (added)
-
tags/1.0.19/src/services/__tests__ (added)
-
tags/1.0.19/src/services/__tests__/api.service.spec.ts (added)
-
tags/1.0.19/src/services/__tests__/auth.service.spec.ts (added)
-
tags/1.0.19/src/services/__tests__/chat-storage.service.spec.ts (added)
-
tags/1.0.19/src/services/__tests__/theme.service.spec.ts (added)
-
tags/1.0.19/src/services/api.service.ts (added)
-
tags/1.0.19/src/services/auth.service.ts (added)
-
tags/1.0.19/src/services/chat-storage.service.ts (added)
-
tags/1.0.19/src/services/theme.service.ts (added)
-
tags/1.0.19/src/types (added)
-
tags/1.0.19/src/types/api.ts (added)
-
tags/1.0.19/src/utils (added)
-
tags/1.0.19/src/utils/product-parser.ts (added)
-
tags/1.0.19/src/utils/utils.spec.ts (added)
-
tags/1.0.19/src/utils/utils.ts (added)
-
tags/1.0.19/uninstall.php (added)
-
trunk/assets/bettercx-widget.esm.js (modified) (1 diff)
-
trunk/assets/index.esm.js (modified) (1 diff)
-
trunk/assets/p-2b7d273b.entry.js (added)
-
trunk/assets/p-3XnA6jB5.system.js (added)
-
trunk/assets/p-3c13f4d3.js (added)
-
trunk/assets/p-5d4e27c5.system.entry.js (added)
-
trunk/assets/p-BTuH8KN8.system.js (added)
-
trunk/assets/p-Bm5fqHLr.js (added)
-
trunk/assets/p-V8up-zPo.system.js (modified) (1 diff)
-
trunk/assets/p-bbbb0d6a.system.entry.js (added)
-
trunk/assets/p-d51d9897.entry.js (added)
-
trunk/bettercx-widget.php (modified) (3 diffs)
-
trunk/readme.txt (modified) (5 diffs)
-
trunk/src/components/bcx-message-composer/bcx-message-composer.scss (modified) (1 diff)
-
trunk/src/components/bettercx-widget/bettercx-widget.scss (modified) (3 diffs)
-
trunk/src/components/bettercx-widget/bettercx-widget.tsx (modified) (7 diffs)
-
trunk/src/services/__tests__/api.service.spec.ts (modified) (6 diffs)
-
trunk/src/services/__tests__/auth.service.spec.ts (modified) (8 diffs)
-
trunk/src/services/api.service.ts (modified) (6 diffs)
-
trunk/src/services/auth.service.ts (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
-
bettercx-widget/trunk/assets/bettercx-widget.esm.js
r3423258 r3435658 1 import{p as e,g as a,b as t}from"./p-BnsX22WT.js";export{s as setNonce}from"./p-BnsX22WT.js";(()=>{const a=import.meta.url,s={};return""!==a&&(s.resourcesUrl=new URL(".",a).href),e(s)})().then((async e=>(await a(),t([["p- 0018ac87",[[257,"bcx-chat-list",{apiService:[16,"api-service"],language:[1],theme:[1],chats:[32],selectedChatId:[32],messages:[32],isLoading:[32],isLoadingMore:[32],hasMore:[32],currentPage:[32],error:[32]},null,{language:["onLanguageChange"]}],[257,"bcx-message-composer",{disabled:[4],loading:[4],placeholder:[1],maxLength:[2,"max-length"],theme:[1],isAttachmentsDisabled:[4,"is-attachments-disabled"],message:[32],images:[32]}],[257,"bcx-product-slider",{products:[16],language:[1],showAfterStreaming:[4,"show-after-streaming"],currentIndex:[32],isVisible:[32]},null,{products:["onProductsChange"],showAfterStreaming:["onShowAfterStreamingChange"]}]]],["p-150b7110",[[257,"bettercx-widget",{publicKey:[1,"public-key"],theme:[1],debug:[4],baseUrl:[1,"base-url"],aiServiceUrl:[1,"ai-service-url"],autoInit:[4,"auto-init"],position:[1],language:[1],embedded:[4],embeddedSize:[1,"embedded-size"],embeddedPlacement:[1,"embedded-placement"],isAttachmentsDisabled:[4,"is-attachments-disabled"],state:[32],timeUpdateTrigger:[32],isDropdownOpen:[32],isFullscreen:[32],showChatList:[32],open:[64],close:[64],toggle:[64],sendMessage:[64]},null,{publicKey:["onPublicKeyChange"],theme:["onThemeChange"],language:["onLanguageChange"],"state.messages":["onMessagesChange"],embedded:["onEmbeddedChange"],embeddedSize:["onEmbeddedSizeChange"],embeddedPlacement:["onEmbeddedPlacementChange"]}]]]],e))));1 import{p as e,g as a,b as t}from"./p-BnsX22WT.js";export{s as setNonce}from"./p-BnsX22WT.js";(()=>{const a=import.meta.url,s={};return""!==a&&(s.resourcesUrl=new URL(".",a).href),e(s)})().then((async e=>(await a(),t([["p-2b7d273b",[[257,"bcx-chat-list",{apiService:[16,"api-service"],language:[1],theme:[1],chats:[32],selectedChatId:[32],messages:[32],isLoading:[32],isLoadingMore:[32],hasMore:[32],currentPage:[32],error:[32]},null,{language:["onLanguageChange"]}],[257,"bcx-message-composer",{disabled:[4],loading:[4],placeholder:[1],maxLength:[2,"max-length"],theme:[1],isAttachmentsDisabled:[4,"is-attachments-disabled"],message:[32],images:[32]}],[257,"bcx-product-slider",{products:[16],language:[1],showAfterStreaming:[4,"show-after-streaming"],currentIndex:[32],isVisible:[32]},null,{products:["onProductsChange"],showAfterStreaming:["onShowAfterStreamingChange"]}]]],["p-d51d9897",[[257,"bettercx-widget",{publicKey:[1,"public-key"],theme:[1],debug:[4],baseUrl:[1,"base-url"],aiServiceUrl:[1,"ai-service-url"],autoInit:[4,"auto-init"],position:[1],language:[1],embedded:[4],embeddedSize:[1,"embedded-size"],embeddedPlacement:[1,"embedded-placement"],isAttachmentsDisabled:[4,"is-attachments-disabled"],state:[32],timeUpdateTrigger:[32],isDropdownOpen:[32],isFullscreen:[32],showChatList:[32],showDelayMessage:[32],delayMessageType:[32],open:[64],close:[64],toggle:[64],sendMessage:[64]},null,{publicKey:["onPublicKeyChange"],theme:["onThemeChange"],language:["onLanguageChange"],"state.messages":["onMessagesChange"],embedded:["onEmbeddedChange"],embeddedSize:["onEmbeddedSizeChange"],embeddedPlacement:["onEmbeddedPlacementChange"]}]]]],e)))); -
bettercx-widget/trunk/assets/index.esm.js
r3423258 r3435658 1 export{a as ApiService,A as AuthService,B as BetterCXWidget,T as ThemeService}from"./p- DdHK2Ifq.js";import"./p-BnsX22WT.js";import"./p-BU1bGO0l.js";function e(e,r,t){return(e||"")+(r?` ${r}`:"")+(t?` ${t}`:"")}export{e as format}1 export{a as ApiService,A as AuthService,B as BetterCXWidget,T as ThemeService}from"./p-Bm5fqHLr.js";import"./p-BnsX22WT.js";import"./p-BU1bGO0l.js";function e(e,r,t){return(e||"")+(r?` ${r}`:"")+(t?` ${t}`:"")}export{e as format} -
bettercx-widget/trunk/assets/p-V8up-zPo.system.js
r3423258 r3435658 1 var __awaiter=this&&this.__awaiter||function(e,t,n,a){function i(e){return e instanceof n?e:new n((function(t){t(e)}))}return new(n||(n=Promise))((function(n,r){function s(e){try{c(a.next(e))}catch(e){r(e)}}function o(e){try{c(a["throw"](e))}catch(e){r(e)}}function c(e){e.done?n(e.value):i(e.value).then(s,o)}c((a=a.apply(e,t||[])).next())}))};var __generator=this&&this.__generator||function(e,t){var n={label:0,sent:function(){if(r[0]&1)throw r[1];return r[1]},trys:[],ops:[]},a,i,r,s;return s={next:o(0),throw:o(1),return:o(2)},typeof Symbol==="function"&&(s[Symbol.iterator]=function(){return this}),s;function o(e){return function(t){return c([e,t])}}function c(o){if(a)throw new TypeError("Generator is already executing.");while(s&&(s=0,o[0]&&(n=0)),n)try{if(a=1,i&&(r=o[0]&2?i["return"]:o[0]?i["throw"]||((r=i["return"])&&r.call(i),0):i.next)&&!(r=r.call(i,o[1])).done)return r;if(i=0,r)o=[o[0]&2,r.value];switch(o[0]){case 0:case 1:r=o;break;case 4:n.label++;return{value:o[1],done:false};case 5:n.label++;i=o[1];o=[0];continue;case 7:o=n.ops.pop();n.trys.pop();continue;default:if(!(r=n.trys,r=r.length>0&&r[r.length-1])&&(o[0]===6||o[0]===2)){n=0;continue}if(o[0]===3&&(!r||o[1]>r[0]&&o[1]<r[3])){n.label=o[1];break}if(o[0]===6&&n.label<r[1]){n.label=r[1];r=o;break}if(r&&n.label<r[2]){n.label=r[2];n.ops.push(o);break}if(r[2])n.ops.pop();n.trys.pop();continue}o=t.call(e,n)}catch(e){o=[6,e];i=0}finally{a=r=0}if(o[0]&5)throw o[1];return{value:o[0]?o[1]:void 0,done:true}}};System.register(["./p-Cbgoi924.system.js"],(function(e,t){"use strict";var n,a,i;return{setters:[function(t){n=t.p;a=t.g;i=t.b;e("setNonce",t.s)}],execute:function(){var e=this;var r=function(){var e=t.meta.url;var a={};if(e!==""){a.resourcesUrl=new URL(".",e).href}return n(a)};r().then((function(t){return __awaiter(e,void 0,void 0,(function(){return __generator(this,(function(e){switch(e.label){case 0:return[4,a()];case 1:e.sent();return[2,i([["p- 31a11738.system",[[257,"bcx-chat-list",{apiService:[16,"api-service"],language:[1],theme:[1],chats:[32],selectedChatId:[32],messages:[32],isLoading:[32],isLoadingMore:[32],hasMore:[32],currentPage:[32],error:[32]},null,{language:["onLanguageChange"]}],[257,"bcx-message-composer",{disabled:[4],loading:[4],placeholder:[1],maxLength:[2,"max-length"],theme:[1],isAttachmentsDisabled:[4,"is-attachments-disabled"],message:[32],images:[32]}],[257,"bcx-product-slider",{products:[16],language:[1],showAfterStreaming:[4,"show-after-streaming"],currentIndex:[32],isVisible:[32]},null,{products:["onProductsChange"],showAfterStreaming:["onShowAfterStreamingChange"]}]]],["p-d434cc1f.system",[[257,"bettercx-widget",{publicKey:[1,"public-key"],theme:[1],debug:[4],baseUrl:[1,"base-url"],aiServiceUrl:[1,"ai-service-url"],autoInit:[4,"auto-init"],position:[1],language:[1],embedded:[4],embeddedSize:[1,"embedded-size"],embeddedPlacement:[1,"embedded-placement"],isAttachmentsDisabled:[4,"is-attachments-disabled"],state:[32],timeUpdateTrigger:[32],isDropdownOpen:[32],isFullscreen:[32],showChatList:[32],open:[64],close:[64],toggle:[64],sendMessage:[64]},null,{publicKey:["onPublicKeyChange"],theme:["onThemeChange"],language:["onLanguageChange"],"state.messages":["onMessagesChange"],embedded:["onEmbeddedChange"],embeddedSize:["onEmbeddedSizeChange"],embeddedPlacement:["onEmbeddedPlacementChange"]}]]]],t)]}}))}))}))}}}));1 var __awaiter=this&&this.__awaiter||function(e,t,n,a){function i(e){return e instanceof n?e:new n((function(t){t(e)}))}return new(n||(n=Promise))((function(n,r){function s(e){try{c(a.next(e))}catch(e){r(e)}}function o(e){try{c(a["throw"](e))}catch(e){r(e)}}function c(e){e.done?n(e.value):i(e.value).then(s,o)}c((a=a.apply(e,t||[])).next())}))};var __generator=this&&this.__generator||function(e,t){var n={label:0,sent:function(){if(r[0]&1)throw r[1];return r[1]},trys:[],ops:[]},a,i,r,s;return s={next:o(0),throw:o(1),return:o(2)},typeof Symbol==="function"&&(s[Symbol.iterator]=function(){return this}),s;function o(e){return function(t){return c([e,t])}}function c(o){if(a)throw new TypeError("Generator is already executing.");while(s&&(s=0,o[0]&&(n=0)),n)try{if(a=1,i&&(r=o[0]&2?i["return"]:o[0]?i["throw"]||((r=i["return"])&&r.call(i),0):i.next)&&!(r=r.call(i,o[1])).done)return r;if(i=0,r)o=[o[0]&2,r.value];switch(o[0]){case 0:case 1:r=o;break;case 4:n.label++;return{value:o[1],done:false};case 5:n.label++;i=o[1];o=[0];continue;case 7:o=n.ops.pop();n.trys.pop();continue;default:if(!(r=n.trys,r=r.length>0&&r[r.length-1])&&(o[0]===6||o[0]===2)){n=0;continue}if(o[0]===3&&(!r||o[1]>r[0]&&o[1]<r[3])){n.label=o[1];break}if(o[0]===6&&n.label<r[1]){n.label=r[1];r=o;break}if(r&&n.label<r[2]){n.label=r[2];n.ops.push(o);break}if(r[2])n.ops.pop();n.trys.pop();continue}o=t.call(e,n)}catch(e){o=[6,e];i=0}finally{a=r=0}if(o[0]&5)throw o[1];return{value:o[0]?o[1]:void 0,done:true}}};System.register(["./p-Cbgoi924.system.js"],(function(e,t){"use strict";var n,a,i;return{setters:[function(t){n=t.p;a=t.g;i=t.b;e("setNonce",t.s)}],execute:function(){var e=this;var r=function(){var e=t.meta.url;var a={};if(e!==""){a.resourcesUrl=new URL(".",e).href}return n(a)};r().then((function(t){return __awaiter(e,void 0,void 0,(function(){return __generator(this,(function(e){switch(e.label){case 0:return[4,a()];case 1:e.sent();return[2,i([["p-5d4e27c5.system",[[257,"bcx-chat-list",{apiService:[16,"api-service"],language:[1],theme:[1],chats:[32],selectedChatId:[32],messages:[32],isLoading:[32],isLoadingMore:[32],hasMore:[32],currentPage:[32],error:[32]},null,{language:["onLanguageChange"]}],[257,"bcx-message-composer",{disabled:[4],loading:[4],placeholder:[1],maxLength:[2,"max-length"],theme:[1],isAttachmentsDisabled:[4,"is-attachments-disabled"],message:[32],images:[32]}],[257,"bcx-product-slider",{products:[16],language:[1],showAfterStreaming:[4,"show-after-streaming"],currentIndex:[32],isVisible:[32]},null,{products:["onProductsChange"],showAfterStreaming:["onShowAfterStreamingChange"]}]]],["p-bbbb0d6a.system",[[257,"bettercx-widget",{publicKey:[1,"public-key"],theme:[1],debug:[4],baseUrl:[1,"base-url"],aiServiceUrl:[1,"ai-service-url"],autoInit:[4,"auto-init"],position:[1],language:[1],embedded:[4],embeddedSize:[1,"embedded-size"],embeddedPlacement:[1,"embedded-placement"],isAttachmentsDisabled:[4,"is-attachments-disabled"],state:[32],timeUpdateTrigger:[32],isDropdownOpen:[32],isFullscreen:[32],showChatList:[32],showDelayMessage:[32],delayMessageType:[32],open:[64],close:[64],toggle:[64],sendMessage:[64]},null,{publicKey:["onPublicKeyChange"],theme:["onThemeChange"],language:["onLanguageChange"],"state.messages":["onMessagesChange"],embedded:["onEmbeddedChange"],embeddedSize:["onEmbeddedSizeChange"],embeddedPlacement:["onEmbeddedPlacementChange"]}]]]],t)]}}))}))}))}}})); -
bettercx-widget/trunk/bettercx-widget.php
r3423258 r3435658 4 4 * Plugin URI: https://wordpress.org/plugins/bettercx-widget/ 5 5 * Description: Professional AI-powered chat widget for BetterCX platform. Seamlessly integrate intelligent customer support into any website with full WordPress compatibility. Fully functional out of the box with no trial limitations. 6 * Version: 1.0.1 86 * Version: 1.0.19 7 7 * Author: BetterCX 8 8 * Author URI: https://bettercx.ai … … 16 16 * 17 17 * @package BetterCX_Widget 18 * @version 1.0.1 818 * @version 1.0.19 19 19 * @author BetterCX 20 20 * @license GPLv2+ … … 37 37 38 38 // Define plugin constants 39 define('BETTERCX_WIDGET_VERSION', '1.0.1 8');39 define('BETTERCX_WIDGET_VERSION', '1.0.19'); 40 40 define('BETTERCX_WIDGET_PLUGIN_FILE', __FILE__); 41 41 define('BETTERCX_WIDGET_PLUGIN_DIR', plugin_dir_path(__FILE__)); -
bettercx-widget/trunk/readme.txt
r3423258 r3435658 5 5 Tested up to: 6.8 6 6 Requires PHP: 7.4 7 Stable tag: 1.0.1 87 Stable tag: 1.0.19 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 248 248 == Changelog == 249 249 250 = 1.0.19 = 251 * Added delay message display when waiting for AI response (after 5 seconds) 252 * Delay message shows "Connecting to assistant" for first-time users and "Thinking..." for returning users 253 * Delay message includes animated dots and disappears when streaming starts 254 * Improved user experience during message processing with visual feedback 255 * Users can now type and add attachments while message is being sent/generated 256 * Fixed spinner animation in send button during message processing 257 250 258 = 1.0.18 = 251 259 * Updated AI service URL to new endpoint … … 379 387 == Upgrade Notice == 380 388 389 = 1.0.19 = 390 Update: Enhanced user experience with delay message display during AI response processing. Users can now type and add attachments while messages are being sent. Improved visual feedback with animated status messages and fixed spinner animation. 391 381 392 = 1.0.18 = 382 393 Update: Updated AI service URL to new endpoint for improved service reliability and performance. … … 658 669 659 670 = Last Updated = 660 2025- 12-11671 2025-01-09 661 672 662 673 = Version = 663 1.0.1 8674 1.0.19 664 675 665 676 = Minimum WordPress Version = … … 676 687 677 688 = Stable Tag = 678 1.0.1 8689 1.0.19 679 690 680 691 = Development Version = 681 1.0.1 8692 1.0.19 682 693 683 694 = Requires at least = -
bettercx-widget/trunk/src/components/bcx-message-composer/bcx-message-composer.scss
r3417078 r3435658 260 260 } 261 261 262 .bcx-composer__spinner { 263 animation: bcx-composer-spin 1s linear infinite; 264 transform-origin: center; 265 } 266 267 @keyframes bcx-composer-spin { 268 from { 269 transform: rotate(0deg); 270 } 271 to { 272 transform: rotate(360deg); 273 } 274 } 275 262 276 @media (max-width: 480px) { 263 277 .bcx-composer__media-btn { -
bettercx-widget/trunk/src/components/bettercx-widget/bettercx-widget.scss
r3420908 r3435658 1355 1355 /* Keep messages and composer aligned horizontally across all modes */ 1356 1356 .bcx-widget__messages, 1357 .bcx-widget__composer { 1357 .bcx-widget__composer, 1358 .bcx-widget__delay-status { 1358 1359 width: 100% !important; 1359 1360 box-sizing: border-box !important; … … 2251 2252 } 2252 2253 2254 /* Delay status message - shown above composer (inspired by ChatGPT) */ 2255 .bcx-widget__delay-status { 2256 display: flex; 2257 align-items: baseline; 2258 justify-content: flex-start; 2259 gap: 4px; 2260 padding: var(--bcx-space-1) var(--bcx-space-4); 2261 margin-bottom: var(--bcx-space-2); 2262 font-size: var(--bcx-text-sm); 2263 font-weight: 500; 2264 font-style: italic; 2265 line-height: 1.5; 2266 background: transparent; 2267 border: none; 2268 animation: bcx-delay-status-appear 0.3s ease-out; 2269 } 2270 2271 .bcx-widget__delay-status-text { 2272 /* Gradient text color - inspired by ping message button */ 2273 background: linear-gradient(135deg, color-mix(in srgb, var(--bcx-primary) 95%, rgba(0, 0, 0, 0.7)) 0%, color-mix(in srgb, var(--bcx-primary) 80%, var(--bcx-white)) 100%); 2274 -webkit-background-clip: text; 2275 margin-left: 4px; 2276 background-clip: text; 2277 -webkit-text-fill-color: transparent; 2278 color: var(--bcx-primary); /* Fallback for browsers that don't support background-clip */ 2279 font-style: italic; 2280 } 2281 2282 .bcx-widget__delay-status-dots { 2283 display: inline-flex; 2284 gap: 4px; 2285 align-items: flex-end; 2286 margin-left: 2px; 2287 } 2288 2289 .bcx-widget__delay-dot { 2290 width: 4px; 2291 height: 4px; 2292 border-radius: var(--bcx-radius-full); 2293 background: linear-gradient(135deg, color-mix(in srgb, var(--bcx-primary) 95%, rgba(0, 0, 0, 0.7)) 0%, color-mix(in srgb, var(--bcx-primary) 80%, var(--bcx-white)) 100%); 2294 opacity: 0.8; 2295 animation: bcx-delay-dot-pulse 1.4s ease-in-out infinite both; 2296 2297 &:nth-child(1) { 2298 animation-delay: -0.4s; 2299 } 2300 &:nth-child(2) { 2301 animation-delay: -0.2s; 2302 } 2303 &:nth-child(3) { 2304 animation-delay: 0s; 2305 } 2306 } 2307 2308 @keyframes bcx-delay-dot-pulse { 2309 0%, 2310 80%, 2311 100% { 2312 opacity: 0.4; 2313 transform: scale(0.8); 2314 } 2315 40% { 2316 opacity: 1; 2317 transform: scale(1); 2318 } 2319 } 2320 2321 @keyframes bcx-delay-status-appear { 2322 from { 2323 opacity: 0; 2324 transform: translateY(-4px); 2325 } 2326 to { 2327 opacity: 1; 2328 transform: translateY(0); 2329 } 2330 } 2331 2253 2332 .bcx-widget__terms-agreement { 2254 2333 padding: var(--bcx-space-3) var(--bcx-space-4) var(--bcx-space-2); … … 2966 3045 2967 3046 .bcx-widget__spinner, 2968 .bcx-widget__typing-indicator span { 3047 .bcx-widget__typing-indicator span, 3048 .bcx-widget__delay-dot { 2969 3049 animation: none; 2970 3050 } -
bettercx-widget/trunk/src/components/bettercx-widget/bettercx-widget.tsx
r3423258 r3435658 86 86 // Track programmatic scrolling to prevent handleScroll from triggering during auto-scroll 87 87 private isProgrammaticScroll: boolean = false; 88 89 // Delay message handling 90 private delayMessageTimeout: ReturnType<typeof setTimeout> | null = null; 91 @State() private showDelayMessage: boolean = false; 92 @State() private delayMessageType: 'selecting' | 'thinking' = null; // 'selecting' = no AI messages, 'thinking' = has AI messages 88 93 89 94 // Events … … 184 189 // Remove click outside listener 185 190 document.removeEventListener('click', this.handleClickOutside); 191 192 // Clean up delay message timeout 193 if (this.delayMessageTimeout) { 194 clearTimeout(this.delayMessageTimeout); 195 this.delayMessageTimeout = null; 196 } 186 197 } 187 198 … … 430 441 this.emitEvent('message-sent', userMessage as unknown as Record<string, unknown>); 431 442 443 // Check if there are any AI messages (excluding welcome message) 444 const hasAiMessages = this.state.messages.some(msg => msg.author === 'assistant' && !msg.id?.startsWith('welcome-')); 445 446 // Set delay message type based on whether there are AI messages 447 const delayMessageType: 'selecting' | 'thinking' = hasAiMessages ? 'thinking' : 'selecting'; 448 449 // Clear any existing delay message timeout 450 if (this.delayMessageTimeout) { 451 clearTimeout(this.delayMessageTimeout); 452 this.delayMessageTimeout = null; 453 } 454 455 // Show delay message after 5 seconds if streaming hasn't started 456 this.delayMessageTimeout = setTimeout(() => { 457 if (this.state.isTyping && !this.showDelayMessage) { 458 this.showDelayMessage = true; 459 this.delayMessageType = delayMessageType; 460 this.setState({}); 461 // Scroll to bottom when delay message appears 462 this.forceScrollToBottom(); 463 } 464 }, 5000); 465 432 466 try { 433 467 const stream = await this.apiService.sendMessage(content, images); … … 442 476 if (chunk.type === 'streaming_output') { 443 477 if (!isStreamingStarted) { 478 // Clear delay message when streaming starts 479 if (this.delayMessageTimeout) { 480 clearTimeout(this.delayMessageTimeout); 481 this.delayMessageTimeout = null; 482 } 483 this.showDelayMessage = false; 484 this.delayMessageType = null; 485 444 486 assistantMessage = { 445 487 content: '', … … 555 597 this.emitEvent('error', { error: error.message }); 556 598 } finally { 599 // Clear delay message timeout on error or completion 600 if (this.delayMessageTimeout) { 601 clearTimeout(this.delayMessageTimeout); 602 this.delayMessageTimeout = null; 603 } 604 this.showDelayMessage = false; 605 this.delayMessageType = null; 557 606 this.setState({ isTyping: false }); 558 607 } … … 865 914 en: 'Instant response', 866 915 pl: 'Natychmiastowa odpowiedź', 916 }, 917 selecting_assistant: { 918 en: 'Connecting to assistant', 919 pl: 'Łączenie z asystentem', 920 }, 921 thinking: { 922 en: 'Working on the answer', 923 pl: 'Pracuję nad odpowiedzią', 867 924 }, 868 925 }; … … 1556 1613 </div> 1557 1614 1615 {/* Delay status message - shown above composer after 5 seconds */} 1616 {this.showDelayMessage && this.delayMessageType && this.state.isTyping && ( 1617 <div class="bcx-widget__delay-status"> 1618 <span class="bcx-widget__delay-status-text"> 1619 {this.delayMessageType === 'selecting' ? this.getTranslation('selecting_assistant') : this.getTranslation('thinking')} 1620 </span> 1621 <span class="bcx-widget__delay-status-dots"> 1622 <span class="bcx-widget__delay-dot"></span> 1623 <span class="bcx-widget__delay-dot"></span> 1624 <span class="bcx-widget__delay-dot"></span> 1625 </span> 1626 </div> 1627 )} 1628 1558 1629 <div class="bcx-widget__composer" data-adblock-bypass="true" role="form" aria-label="Message composer"> 1559 1630 <bcx-message-composer 1560 1631 onMessageSubmit={this.handleMessageSubmit} 1561 disabled={ this.state.isTyping}1632 disabled={false} 1562 1633 loading={this.state.isTyping} 1563 1634 placeholder={this.getTranslation('message_placeholder')} -
bettercx-widget/trunk/src/services/__tests__/api.service.spec.ts
r3423258 r3435658 24 24 describe('getWidgetConfig', () => { 25 25 it('should fetch config successfully', async () => { 26 (mockAuthService.getToken as jest.Mock).mockRe turnValue('fake-token');27 (mockAuthService.getAuthHeader as jest.Mock).mockRe turnValue({ Authorization: 'Bearer fake-token' });26 (mockAuthService.getToken as jest.Mock).mockResolvedValue('fake-token'); 27 (mockAuthService.getAuthHeader as jest.Mock).mockResolvedValue({ Authorization: 'Bearer fake-token' }); 28 28 29 29 const mockResponse = { attrs: { light_mode: {} } }; … … 40 40 41 41 it('should throw error if no token', async () => { 42 (mockAuthService.getToken as jest.Mock).mockRe turnValue(null);42 (mockAuthService.getToken as jest.Mock).mockResolvedValue(undefined); 43 43 44 44 await expect(service.getWidgetConfig(123)).rejects.toThrow('No valid session token available'); … … 48 48 describe('sendMessage', () => { 49 49 it('should send message as JSON when no images', async () => { 50 (mockAuthService.getToken as jest.Mock).mockRe turnValue('fake-token');51 (mockAuthService.getAllHeaders as jest.Mock).mockRe turnValue({ Authorization: 'Bearer fake-token' });50 (mockAuthService.getToken as jest.Mock).mockResolvedValue('fake-token'); 51 (mockAuthService.getAllHeaders as jest.Mock).mockResolvedValue({ Authorization: 'Bearer fake-token' }); 52 52 53 53 const mockStream = new ReadableStream(); … … 66 66 headers: expect.objectContaining({ 'Content-Type': 'application/json' }), 67 67 body: JSON.stringify({ content: 'Hello' }), 68 credentials: 'omit', 68 69 }), 69 70 ); … … 72 73 73 74 it('should send message as FormData when images are present', async () => { 74 (mockAuthService.getToken as jest.Mock).mockReturnValue('fake-token'); 75 (mockAuthService.getToken as jest.Mock).mockResolvedValue('fake-token'); 76 (mockAuthService.getAllHeaders as jest.Mock).mockResolvedValue({ Authorization: 'Bearer fake-token' }); 75 77 76 78 const mockStream = new ReadableStream(); … … 94 96 describe('getChatMessages', () => { 95 97 it('should fetch paginated messages', async () => { 96 (mockAuthService.getToken as jest.Mock).mockReturnValue('fake-token'); 98 (mockAuthService.getToken as jest.Mock).mockResolvedValue('fake-token'); 99 (mockAuthService.getAuthHeader as jest.Mock).mockResolvedValue({ Authorization: 'Bearer fake-token' }); 97 100 98 101 const mockData = { -
bettercx-widget/trunk/src/services/__tests__/auth.service.spec.ts
r3417078 r3435658 49 49 expect(result).toEqual(mockResponse.data); 50 50 // The token should be stored and valid after successful session creation 51 expect(a uthService.getToken()).toBe('mock-jwt-token');51 expect(await authService.getToken()).toBe('mock-jwt-token'); 52 52 expect(authService.isTokenValid()).toBe(true); 53 53 }); … … 74 74 75 75 describe('token management', () => { 76 it('should return undefined for invalid token', () => {77 expect(a uthService.getToken()).toBeUndefined();76 it('should return undefined for invalid token', async () => { 77 expect(await authService.getToken()).toBeUndefined(); 78 78 expect(authService.isTokenValid()).toBe(false); 79 79 }); 80 80 81 it('should clear session', () => {81 it('should clear session', async () => { 82 82 // Set a mock token 83 83 const authServiceWithPrivate = authService as unknown as { sessionToken?: string; sessionExpiresAt?: Date }; … … 87 87 authService.clearSession(); 88 88 89 expect(a uthService.getToken()).toBeUndefined();89 expect(await authService.getToken()).toBeUndefined(); 90 90 expect(authService.isTokenValid()).toBe(false); 91 91 }); 92 92 93 it('should return auth header when token is valid', () => {93 it('should return auth header when token is valid', async () => { 94 94 const authServiceWithPrivate = authService as unknown as { sessionToken?: string; sessionExpiresAt?: Date }; 95 95 authServiceWithPrivate.sessionToken = 'mock-token'; 96 96 authServiceWithPrivate.sessionExpiresAt = new Date(Date.now() + 3600000); 97 97 98 expect(a uthService.getAuthHeader()).toEqual({98 expect(await authService.getAuthHeader()).toEqual({ 99 99 Authorization: 'Bearer mock-token', 100 100 }); 101 101 }); 102 102 103 it('should return empty object when token is invalid', () => {104 expect(a uthService.getAuthHeader()).toEqual({});105 }); 106 107 it('should return undefined when token is expired', () => {103 it('should return empty object when token is invalid', async () => { 104 expect(await authService.getAuthHeader()).toEqual({}); 105 }); 106 107 it('should return undefined when token is expired', async () => { 108 108 // Set a mock token with past expiration 109 109 const authServiceWithPrivate = authService as unknown as { sessionToken?: string; sessionExpiresAt?: Date }; … … 111 111 authServiceWithPrivate.sessionExpiresAt = new Date(Date.now() - 3600000); 112 112 113 expect(a uthService.getToken()).toBeUndefined();113 expect(await authService.getToken()).toBeUndefined(); 114 114 expect(authService.isTokenValid()).toBe(false); 115 115 }); … … 148 148 expect(result).toBe(true); 149 149 expect(mockFetch).toHaveBeenCalled(); 150 expect(a uthService.getToken()).toBe('new-mock-token');150 expect(await authService.getToken()).toBe('new-mock-token'); 151 151 }); 152 152 … … 186 186 }); 187 187 188 it('should return all headers including chat ID', () => {188 it('should return all headers including chat ID', async () => { 189 189 const authServiceWithPrivate = authService as unknown as { sessionToken?: string; sessionExpiresAt?: Date }; 190 190 authServiceWithPrivate.sessionToken = 'mock-token'; … … 193 193 authService.setChatId(chatId); 194 194 195 expect(a uthService.getAllHeaders()).toEqual({195 expect(await authService.getAllHeaders()).toEqual({ 196 196 'Authorization': 'Bearer mock-token', 197 197 'X-BCX-Chat-ID': chatId, … … 210 210 }); 211 211 }); 212 213 describe('automatic token refresh', () => { 214 it('should automatically refresh token when expired in getToken()', async () => { 215 // Set expired token 216 const authServiceWithPrivate = authService as unknown as { 217 sessionToken?: string; 218 sessionExpiresAt?: Date; 219 widgetKey?: string; 220 origin?: string; 221 }; 222 authServiceWithPrivate.sessionToken = 'expired-token'; 223 authServiceWithPrivate.sessionExpiresAt = new Date(Date.now() - 3600000); // 1 hour ago 224 authServiceWithPrivate.widgetKey = 'pk_test_key'; 225 authServiceWithPrivate.origin = 'https://example.com'; 226 227 // Mock refresh response 228 const mockResponse = { 229 status: 'success', 230 message: 'Session created successfully', 231 data: { 232 token: 'new-refreshed-token', 233 expires_at: new Date(Date.now() + 3600000).toISOString(), // 1 hour from now 234 session_id: 'new-session-id', 235 }, 236 }; 237 238 mockFetch.mockResolvedValueOnce({ 239 ok: true, 240 json: async () => mockResponse, 241 } as Response); 242 243 // getToken() should automatically refresh 244 const token = await authService.getToken(); 245 246 expect(token).toBe('new-refreshed-token'); 247 expect(mockFetch).toHaveBeenCalled(); 248 expect(authService.isTokenValid()).toBe(true); 249 }); 250 251 it('should not refresh if token is still valid', async () => { 252 // Set valid token 253 const authServiceWithPrivate = authService as unknown as { 254 sessionToken?: string; 255 sessionExpiresAt?: Date; 256 widgetKey?: string; 257 origin?: string; 258 }; 259 authServiceWithPrivate.sessionToken = 'valid-token'; 260 authServiceWithPrivate.sessionExpiresAt = new Date(Date.now() + 3600000); // 1 hour from now 261 authServiceWithPrivate.widgetKey = 'pk_test_key'; 262 authServiceWithPrivate.origin = 'https://example.com'; 263 264 const token = await authService.getToken(); 265 266 expect(token).toBe('valid-token'); 267 expect(mockFetch).not.toHaveBeenCalled(); 268 }); 269 270 it('should return undefined if refresh fails', async () => { 271 // Set expired token 272 const authServiceWithPrivate = authService as unknown as { 273 sessionToken?: string; 274 sessionExpiresAt?: Date; 275 widgetKey?: string; 276 origin?: string; 277 }; 278 authServiceWithPrivate.sessionToken = 'expired-token'; 279 authServiceWithPrivate.sessionExpiresAt = new Date(Date.now() - 3600000); 280 authServiceWithPrivate.widgetKey = 'pk_test_key'; 281 authServiceWithPrivate.origin = 'https://example.com'; 282 283 // Mock failed refresh 284 mockFetch.mockRejectedValueOnce(new Error('Network error')); 285 286 const token = await authService.getToken(); 287 288 expect(token).toBeUndefined(); 289 expect(mockFetch).toHaveBeenCalled(); 290 }); 291 292 it('should prevent concurrent refresh attempts', async () => { 293 // Set expired token 294 const authServiceWithPrivate = authService as unknown as { 295 sessionToken?: string; 296 sessionExpiresAt?: Date; 297 widgetKey?: string; 298 origin?: string; 299 }; 300 authServiceWithPrivate.sessionToken = 'expired-token'; 301 authServiceWithPrivate.sessionExpiresAt = new Date(Date.now() - 3600000); 302 authServiceWithPrivate.widgetKey = 'pk_test_key'; 303 authServiceWithPrivate.origin = 'https://example.com'; 304 305 // Mock slow refresh response 306 const mockResponse = { 307 status: 'success', 308 message: 'Session created successfully', 309 data: { 310 token: 'new-refreshed-token', 311 expires_at: new Date(Date.now() + 3600000).toISOString(), 312 session_id: 'new-session-id', 313 }, 314 }; 315 316 let resolveFetch: (value: Response) => void; 317 const slowFetchPromise = new Promise<Response>(resolve => { 318 resolveFetch = resolve; 319 }); 320 321 mockFetch.mockReturnValueOnce(slowFetchPromise as Promise<Response>); 322 323 // Start multiple concurrent getToken() calls 324 const tokenPromises = [ 325 authService.getToken(), 326 authService.getToken(), 327 authService.getToken(), 328 ]; 329 330 // Resolve fetch after a delay 331 setTimeout(() => { 332 resolveFetch!({ 333 ok: true, 334 json: async () => mockResponse, 335 } as Response); 336 }, 100); 337 338 const tokens = await Promise.all(tokenPromises); 339 340 // All should return the same refreshed token 341 expect(tokens.every(t => t === 'new-refreshed-token')).toBe(true); 342 // Fetch should only be called once (not 3 times) 343 expect(mockFetch).toHaveBeenCalledTimes(1); 344 }); 345 }); 212 346 }); -
bettercx-widget/trunk/src/services/api.service.ts
r3423258 r3435658 22 22 */ 23 23 async getWidgetConfig(organizationId: number): Promise<OrganizationWidgetConfiguration> { 24 const token = this.authService.getToken();24 const token = await this.authService.getToken(); 25 25 if (!token) { 26 26 throw new Error('No valid session token available'); … … 31 31 headers: { 32 32 'Content-Type': 'application/json', 33 ... this.authService.getAuthHeader(),33 ...(await this.authService.getAuthHeader()), 34 34 }, 35 35 }); … … 47 47 */ 48 48 async sendMessage(message: string, images?: File[]): Promise<ReadableStream<Uint8Array> | null> { 49 const token = this.authService.getToken();49 const token = await this.authService.getToken(); 50 50 51 51 if (!token) { … … 54 54 55 55 const headers = { 56 ... this.authService.getAllHeaders(), // Use all headers (auth + chat ID)56 ...(await this.authService.getAllHeaders()), // Use all headers (auth + chat ID) 57 57 }; 58 58 … … 162 162 */ 163 163 async getChatMessages(chatId: string, page: number = 1, pageSize: number = 20): Promise<PaginatedMessagesResponse> { 164 const token = this.authService.getToken();164 const token = await this.authService.getToken(); 165 165 if (!token) { 166 166 throw new Error('No valid session token available'); … … 177 177 headers: { 178 178 'Content-Type': 'application/json', 179 ... this.authService.getAuthHeader(),179 ...(await this.authService.getAuthHeader()), 180 180 }, 181 181 }); -
bettercx-widget/trunk/src/services/auth.service.ts
r3417078 r3435658 11 11 private sessionExpiresAt?: Date; 12 12 private chatId?: string; // Store chat ID for conversation context 13 private widgetKey?: string; // Store widget key for token refresh 14 private origin?: string; // Store origin for token refresh 15 private refreshPromise?: Promise<boolean>; // Promise for ongoing refresh to prevent concurrent refreshes 13 16 14 17 constructor(baseUrl: string = 'http://localhost:8000') { … … 20 23 */ 21 24 async createSession(widgetKey: string, origin: string): Promise<Partial<WidgetSessionResponse>> { 25 // Store widget key and origin for automatic token refresh 26 this.widgetKey = widgetKey; 27 this.origin = origin; 28 22 29 const request: WidgetSessionRequest = { 23 30 widget_key: widgetKey, … … 63 70 /** 64 71 * Get current session token 72 * Automatically refreshes token if expired (seamless token management) 73 * This ensures the widget continues to work even after long periods of inactivity 65 74 */ 66 getToken(): string | undefined { 75 async getToken(): Promise<string | undefined> { 76 // If token is valid, return it immediately 67 77 if (this.isTokenValid()) { 68 78 return this.sessionToken; 69 79 } 80 81 // If token expired and we have credentials, try to refresh 82 if (this.widgetKey && this.origin) { 83 // If refresh is already in progress, wait for it 84 if (this.refreshPromise) { 85 const refreshed = await this.refreshPromise; 86 if (refreshed && this.isTokenValid()) { 87 return this.sessionToken; 88 } 89 return undefined; 90 } 91 92 // Start refresh process 93 this.refreshPromise = this.refreshSessionIfNeeded(this.widgetKey, this.origin); 94 95 try { 96 const refreshed = await this.refreshPromise; 97 this.refreshPromise = undefined; // Clear promise after completion 98 99 if (refreshed && this.isTokenValid()) { 100 return this.sessionToken; 101 } 102 } catch { 103 this.refreshPromise = undefined; // Clear promise on error 104 // Refresh failed, return undefined 105 // Error will be handled by calling code 106 } 107 } 108 109 // No credentials available or refresh failed 70 110 return undefined; 71 111 } … … 88 128 this.sessionExpiresAt = undefined; 89 129 this.chatId = undefined; // Clear chat ID as well 130 this.widgetKey = undefined; // Clear widget key 131 this.origin = undefined; // Clear origin 132 this.refreshPromise = undefined; // Clear any pending refresh 90 133 } 91 134 92 135 /** 93 136 * Get authorization header for API requests 137 * Automatically refreshes token if expired 94 138 */ 95 getAuthHeader(): { Authorization: string } | {}{96 const token = this.getToken();139 async getAuthHeader(): Promise<{ Authorization: string } | {}> { 140 const token = await this.getToken(); 97 141 const headers = token ? { Authorization: `Bearer ${token}` } : {}; 98 142 return headers; … … 109 153 /** 110 154 * Get all headers for API requests (auth + chat ID) 155 * Automatically refreshes token if expired 111 156 */ 112 getAllHeaders(): Record<string, string> {157 async getAllHeaders(): Promise<Record<string, string>> { 113 158 const headers = { 114 ... this.getAuthHeader(),159 ...(await this.getAuthHeader()), 115 160 ...this.getChatIdHeader(), 116 161 };
Note: See TracChangeset
for help on using the changeset viewer.