@repzo/widget-react-native 1.0.0

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 (448) hide show
  1. package/README.md +482 -0
  2. package/lib/commonjs/agent-ui-blocks.js +159 -0
  3. package/lib/commonjs/agent-ui-blocks.js.map +1 -0
  4. package/lib/commonjs/api/client.js +267 -0
  5. package/lib/commonjs/api/client.js.map +1 -0
  6. package/lib/commonjs/api/realtime.js +193 -0
  7. package/lib/commonjs/api/realtime.js.map +1 -0
  8. package/lib/commonjs/api/sse-parser.js +58 -0
  9. package/lib/commonjs/api/sse-parser.js.map +1 -0
  10. package/lib/commonjs/controller.js +23 -0
  11. package/lib/commonjs/controller.js.map +1 -0
  12. package/lib/commonjs/device-id.js +75 -0
  13. package/lib/commonjs/device-id.js.map +1 -0
  14. package/lib/commonjs/events.js +49 -0
  15. package/lib/commonjs/events.js.map +1 -0
  16. package/lib/commonjs/i18n.js +106 -0
  17. package/lib/commonjs/i18n.js.map +1 -0
  18. package/lib/commonjs/index.js +98 -0
  19. package/lib/commonjs/index.js.map +1 -0
  20. package/lib/commonjs/locales.js +85 -0
  21. package/lib/commonjs/locales.js.map +1 -0
  22. package/lib/commonjs/package.json +1 -0
  23. package/lib/commonjs/push/push-notifications.js +208 -0
  24. package/lib/commonjs/push/push-notifications.js.map +1 -0
  25. package/lib/commonjs/state/use-widget-state.js +23 -0
  26. package/lib/commonjs/state/use-widget-state.js.map +1 -0
  27. package/lib/commonjs/state/widget-store.js +1682 -0
  28. package/lib/commonjs/state/widget-store.js.map +1 -0
  29. package/lib/commonjs/storage.js +147 -0
  30. package/lib/commonjs/storage.js.map +1 -0
  31. package/lib/commonjs/types.js +44 -0
  32. package/lib/commonjs/types.js.map +1 -0
  33. package/lib/commonjs/ui/WidgetProvider.js +404 -0
  34. package/lib/commonjs/ui/WidgetProvider.js.map +1 -0
  35. package/lib/commonjs/ui/components/AgentUiBlocks.js +864 -0
  36. package/lib/commonjs/ui/components/AgentUiBlocks.js.map +1 -0
  37. package/lib/commonjs/ui/components/Avatar.js +76 -0
  38. package/lib/commonjs/ui/components/Avatar.js.map +1 -0
  39. package/lib/commonjs/ui/components/ConversationRow.js +155 -0
  40. package/lib/commonjs/ui/components/ConversationRow.js.map +1 -0
  41. package/lib/commonjs/ui/components/ErrorBoundary.js +44 -0
  42. package/lib/commonjs/ui/components/ErrorBoundary.js.map +1 -0
  43. package/lib/commonjs/ui/components/Gradient.js +168 -0
  44. package/lib/commonjs/ui/components/Gradient.js.map +1 -0
  45. package/lib/commonjs/ui/components/Launcher.js +125 -0
  46. package/lib/commonjs/ui/components/Launcher.js.map +1 -0
  47. package/lib/commonjs/ui/components/Markdown.js +257 -0
  48. package/lib/commonjs/ui/components/Markdown.js.map +1 -0
  49. package/lib/commonjs/ui/components/OutboundMessageCard.js +157 -0
  50. package/lib/commonjs/ui/components/OutboundMessageCard.js.map +1 -0
  51. package/lib/commonjs/ui/components/WidgetPanel.js +131 -0
  52. package/lib/commonjs/ui/components/WidgetPanel.js.map +1 -0
  53. package/lib/commonjs/ui/components/icons.js +383 -0
  54. package/lib/commonjs/ui/components/icons.js.map +1 -0
  55. package/lib/commonjs/ui/hooks/use-keyboard-overlap.js +113 -0
  56. package/lib/commonjs/ui/hooks/use-keyboard-overlap.js.map +1 -0
  57. package/lib/commonjs/ui/hooks/use-reduce-motion.js +38 -0
  58. package/lib/commonjs/ui/hooks/use-reduce-motion.js.map +1 -0
  59. package/lib/commonjs/ui/hooks/use-widget-insets.js +41 -0
  60. package/lib/commonjs/ui/hooks/use-widget-insets.js.map +1 -0
  61. package/lib/commonjs/ui/screens/chat/ChatView.js +590 -0
  62. package/lib/commonjs/ui/screens/chat/ChatView.js.map +1 -0
  63. package/lib/commonjs/ui/screens/chat/ChatView.styles.js +362 -0
  64. package/lib/commonjs/ui/screens/chat/ChatView.styles.js.map +1 -0
  65. package/lib/commonjs/ui/screens/chat/ChatView.types.js +6 -0
  66. package/lib/commonjs/ui/screens/chat/ChatView.types.js.map +1 -0
  67. package/lib/commonjs/ui/screens/conversations/ConversationsList.js +150 -0
  68. package/lib/commonjs/ui/screens/conversations/ConversationsList.js.map +1 -0
  69. package/lib/commonjs/ui/screens/conversations/ConversationsList.styles.js +118 -0
  70. package/lib/commonjs/ui/screens/conversations/ConversationsList.styles.js.map +1 -0
  71. package/lib/commonjs/ui/screens/conversations/ConversationsList.types.js +6 -0
  72. package/lib/commonjs/ui/screens/conversations/ConversationsList.types.js.map +1 -0
  73. package/lib/commonjs/ui/screens/home/HomeView.js +336 -0
  74. package/lib/commonjs/ui/screens/home/HomeView.js.map +1 -0
  75. package/lib/commonjs/ui/screens/home/HomeView.styles.js +245 -0
  76. package/lib/commonjs/ui/screens/home/HomeView.styles.js.map +1 -0
  77. package/lib/commonjs/ui/screens/home/HomeView.types.js +6 -0
  78. package/lib/commonjs/ui/screens/home/HomeView.types.js.map +1 -0
  79. package/lib/commonjs/ui/screens/home/greeting.js +49 -0
  80. package/lib/commonjs/ui/screens/home/greeting.js.map +1 -0
  81. package/lib/commonjs/ui/screens/pre-chat/PreChatForm.js +229 -0
  82. package/lib/commonjs/ui/screens/pre-chat/PreChatForm.js.map +1 -0
  83. package/lib/commonjs/ui/screens/pre-chat/PreChatForm.styles.js +125 -0
  84. package/lib/commonjs/ui/screens/pre-chat/PreChatForm.styles.js.map +1 -0
  85. package/lib/commonjs/ui/screens/pre-chat/PreChatForm.types.js +6 -0
  86. package/lib/commonjs/ui/screens/pre-chat/PreChatForm.types.js.map +1 -0
  87. package/lib/commonjs/ui/styles/global.js +55 -0
  88. package/lib/commonjs/ui/styles/global.js.map +1 -0
  89. package/lib/commonjs/ui/styles/theme.js +148 -0
  90. package/lib/commonjs/ui/styles/theme.js.map +1 -0
  91. package/lib/commonjs/ui/utils/conversations.js +125 -0
  92. package/lib/commonjs/ui/utils/conversations.js.map +1 -0
  93. package/lib/commonjs/ui/utils/html.js +44 -0
  94. package/lib/commonjs/ui/utils/html.js.map +1 -0
  95. package/lib/commonjs/ui/utils/native-gradient.js +65 -0
  96. package/lib/commonjs/ui/utils/native-gradient.js.map +1 -0
  97. package/lib/commonjs/ui/utils/rtl.js +56 -0
  98. package/lib/commonjs/ui/utils/rtl.js.map +1 -0
  99. package/lib/commonjs/ui/utils/safe-url.js +67 -0
  100. package/lib/commonjs/ui/utils/safe-url.js.map +1 -0
  101. package/lib/module/agent-ui-blocks.js +152 -0
  102. package/lib/module/agent-ui-blocks.js.map +1 -0
  103. package/lib/module/api/client.js +261 -0
  104. package/lib/module/api/client.js.map +1 -0
  105. package/lib/module/api/realtime.js +189 -0
  106. package/lib/module/api/realtime.js.map +1 -0
  107. package/lib/module/api/sse-parser.js +53 -0
  108. package/lib/module/api/sse-parser.js.map +1 -0
  109. package/lib/module/controller.js +19 -0
  110. package/lib/module/controller.js.map +1 -0
  111. package/lib/module/device-id.js +69 -0
  112. package/lib/module/device-id.js.map +1 -0
  113. package/lib/module/events.js +44 -0
  114. package/lib/module/events.js.map +1 -0
  115. package/lib/module/i18n.js +99 -0
  116. package/lib/module/i18n.js.map +1 -0
  117. package/lib/module/index.js +55 -0
  118. package/lib/module/index.js.map +1 -0
  119. package/lib/module/locales.js +80 -0
  120. package/lib/module/locales.js.map +1 -0
  121. package/lib/module/package.json +1 -0
  122. package/lib/module/push/push-notifications.js +204 -0
  123. package/lib/module/push/push-notifications.js.map +1 -0
  124. package/lib/module/state/use-widget-state.js +18 -0
  125. package/lib/module/state/use-widget-state.js.map +1 -0
  126. package/lib/module/state/widget-store.js +1677 -0
  127. package/lib/module/state/widget-store.js.map +1 -0
  128. package/lib/module/storage.js +141 -0
  129. package/lib/module/storage.js.map +1 -0
  130. package/lib/module/types.js +40 -0
  131. package/lib/module/types.js.map +1 -0
  132. package/lib/module/ui/WidgetProvider.js +400 -0
  133. package/lib/module/ui/WidgetProvider.js.map +1 -0
  134. package/lib/module/ui/components/AgentUiBlocks.js +857 -0
  135. package/lib/module/ui/components/AgentUiBlocks.js.map +1 -0
  136. package/lib/module/ui/components/Avatar.js +71 -0
  137. package/lib/module/ui/components/Avatar.js.map +1 -0
  138. package/lib/module/ui/components/ConversationRow.js +150 -0
  139. package/lib/module/ui/components/ConversationRow.js.map +1 -0
  140. package/lib/module/ui/components/ErrorBoundary.js +39 -0
  141. package/lib/module/ui/components/ErrorBoundary.js.map +1 -0
  142. package/lib/module/ui/components/Gradient.js +165 -0
  143. package/lib/module/ui/components/Gradient.js.map +1 -0
  144. package/lib/module/ui/components/Launcher.js +121 -0
  145. package/lib/module/ui/components/Launcher.js.map +1 -0
  146. package/lib/module/ui/components/Markdown.js +253 -0
  147. package/lib/module/ui/components/Markdown.js.map +1 -0
  148. package/lib/module/ui/components/OutboundMessageCard.js +153 -0
  149. package/lib/module/ui/components/OutboundMessageCard.js.map +1 -0
  150. package/lib/module/ui/components/WidgetPanel.js +127 -0
  151. package/lib/module/ui/components/WidgetPanel.js.map +1 -0
  152. package/lib/module/ui/components/icons.js +370 -0
  153. package/lib/module/ui/components/icons.js.map +1 -0
  154. package/lib/module/ui/hooks/use-keyboard-overlap.js +109 -0
  155. package/lib/module/ui/hooks/use-keyboard-overlap.js.map +1 -0
  156. package/lib/module/ui/hooks/use-reduce-motion.js +34 -0
  157. package/lib/module/ui/hooks/use-reduce-motion.js.map +1 -0
  158. package/lib/module/ui/hooks/use-widget-insets.js +37 -0
  159. package/lib/module/ui/hooks/use-widget-insets.js.map +1 -0
  160. package/lib/module/ui/screens/chat/ChatView.js +586 -0
  161. package/lib/module/ui/screens/chat/ChatView.js.map +1 -0
  162. package/lib/module/ui/screens/chat/ChatView.styles.js +358 -0
  163. package/lib/module/ui/screens/chat/ChatView.styles.js.map +1 -0
  164. package/lib/module/ui/screens/chat/ChatView.types.js +4 -0
  165. package/lib/module/ui/screens/chat/ChatView.types.js.map +1 -0
  166. package/lib/module/ui/screens/conversations/ConversationsList.js +146 -0
  167. package/lib/module/ui/screens/conversations/ConversationsList.js.map +1 -0
  168. package/lib/module/ui/screens/conversations/ConversationsList.styles.js +114 -0
  169. package/lib/module/ui/screens/conversations/ConversationsList.styles.js.map +1 -0
  170. package/lib/module/ui/screens/conversations/ConversationsList.types.js +4 -0
  171. package/lib/module/ui/screens/conversations/ConversationsList.types.js.map +1 -0
  172. package/lib/module/ui/screens/home/HomeView.js +332 -0
  173. package/lib/module/ui/screens/home/HomeView.js.map +1 -0
  174. package/lib/module/ui/screens/home/HomeView.styles.js +242 -0
  175. package/lib/module/ui/screens/home/HomeView.styles.js.map +1 -0
  176. package/lib/module/ui/screens/home/HomeView.types.js +4 -0
  177. package/lib/module/ui/screens/home/HomeView.types.js.map +1 -0
  178. package/lib/module/ui/screens/home/greeting.js +44 -0
  179. package/lib/module/ui/screens/home/greeting.js.map +1 -0
  180. package/lib/module/ui/screens/pre-chat/PreChatForm.js +225 -0
  181. package/lib/module/ui/screens/pre-chat/PreChatForm.js.map +1 -0
  182. package/lib/module/ui/screens/pre-chat/PreChatForm.styles.js +121 -0
  183. package/lib/module/ui/screens/pre-chat/PreChatForm.styles.js.map +1 -0
  184. package/lib/module/ui/screens/pre-chat/PreChatForm.types.js +4 -0
  185. package/lib/module/ui/screens/pre-chat/PreChatForm.types.js.map +1 -0
  186. package/lib/module/ui/styles/global.js +51 -0
  187. package/lib/module/ui/styles/global.js.map +1 -0
  188. package/lib/module/ui/styles/theme.js +137 -0
  189. package/lib/module/ui/styles/theme.js.map +1 -0
  190. package/lib/module/ui/utils/conversations.js +115 -0
  191. package/lib/module/ui/utils/conversations.js.map +1 -0
  192. package/lib/module/ui/utils/html.js +39 -0
  193. package/lib/module/ui/utils/html.js.map +1 -0
  194. package/lib/module/ui/utils/native-gradient.js +60 -0
  195. package/lib/module/ui/utils/native-gradient.js.map +1 -0
  196. package/lib/module/ui/utils/rtl.js +49 -0
  197. package/lib/module/ui/utils/rtl.js.map +1 -0
  198. package/lib/module/ui/utils/safe-url.js +59 -0
  199. package/lib/module/ui/utils/safe-url.js.map +1 -0
  200. package/lib/typescript/commonjs/agent-ui-blocks.d.ts +20 -0
  201. package/lib/typescript/commonjs/agent-ui-blocks.d.ts.map +1 -0
  202. package/lib/typescript/commonjs/api/client.d.ts +115 -0
  203. package/lib/typescript/commonjs/api/client.d.ts.map +1 -0
  204. package/lib/typescript/commonjs/api/realtime.d.ts +39 -0
  205. package/lib/typescript/commonjs/api/realtime.d.ts.map +1 -0
  206. package/lib/typescript/commonjs/api/sse-parser.d.ts +13 -0
  207. package/lib/typescript/commonjs/api/sse-parser.d.ts.map +1 -0
  208. package/lib/typescript/commonjs/controller.d.ts +15 -0
  209. package/lib/typescript/commonjs/controller.d.ts.map +1 -0
  210. package/lib/typescript/commonjs/device-id.d.ts +15 -0
  211. package/lib/typescript/commonjs/device-id.d.ts.map +1 -0
  212. package/lib/typescript/commonjs/events.d.ts +41 -0
  213. package/lib/typescript/commonjs/events.d.ts.map +1 -0
  214. package/lib/typescript/commonjs/i18n.d.ts +78 -0
  215. package/lib/typescript/commonjs/i18n.d.ts.map +1 -0
  216. package/lib/typescript/commonjs/index.d.ts +103 -0
  217. package/lib/typescript/commonjs/index.d.ts.map +1 -0
  218. package/lib/typescript/commonjs/locales.d.ts +18 -0
  219. package/lib/typescript/commonjs/locales.d.ts.map +1 -0
  220. package/lib/typescript/commonjs/package.json +1 -0
  221. package/lib/typescript/commonjs/push/push-notifications.d.ts +71 -0
  222. package/lib/typescript/commonjs/push/push-notifications.d.ts.map +1 -0
  223. package/lib/typescript/commonjs/state/use-widget-state.d.ts +10 -0
  224. package/lib/typescript/commonjs/state/use-widget-state.d.ts.map +1 -0
  225. package/lib/typescript/commonjs/state/widget-store.d.ts +227 -0
  226. package/lib/typescript/commonjs/state/widget-store.d.ts.map +1 -0
  227. package/lib/typescript/commonjs/storage.d.ts +42 -0
  228. package/lib/typescript/commonjs/storage.d.ts.map +1 -0
  229. package/lib/typescript/commonjs/types.d.ts +391 -0
  230. package/lib/typescript/commonjs/types.d.ts.map +1 -0
  231. package/lib/typescript/commonjs/ui/WidgetProvider.d.ts +15 -0
  232. package/lib/typescript/commonjs/ui/WidgetProvider.d.ts.map +1 -0
  233. package/lib/typescript/commonjs/ui/components/AgentUiBlocks.d.ts +27 -0
  234. package/lib/typescript/commonjs/ui/components/AgentUiBlocks.d.ts.map +1 -0
  235. package/lib/typescript/commonjs/ui/components/Avatar.d.ts +19 -0
  236. package/lib/typescript/commonjs/ui/components/Avatar.d.ts.map +1 -0
  237. package/lib/typescript/commonjs/ui/components/ConversationRow.d.ts +26 -0
  238. package/lib/typescript/commonjs/ui/components/ConversationRow.d.ts.map +1 -0
  239. package/lib/typescript/commonjs/ui/components/ErrorBoundary.d.ts +27 -0
  240. package/lib/typescript/commonjs/ui/components/ErrorBoundary.d.ts.map +1 -0
  241. package/lib/typescript/commonjs/ui/components/Gradient.d.ts +33 -0
  242. package/lib/typescript/commonjs/ui/components/Gradient.d.ts.map +1 -0
  243. package/lib/typescript/commonjs/ui/components/Launcher.d.ts +18 -0
  244. package/lib/typescript/commonjs/ui/components/Launcher.d.ts.map +1 -0
  245. package/lib/typescript/commonjs/ui/components/Markdown.d.ts +19 -0
  246. package/lib/typescript/commonjs/ui/components/Markdown.d.ts.map +1 -0
  247. package/lib/typescript/commonjs/ui/components/OutboundMessageCard.d.ts +20 -0
  248. package/lib/typescript/commonjs/ui/components/OutboundMessageCard.d.ts.map +1 -0
  249. package/lib/typescript/commonjs/ui/components/WidgetPanel.d.ts +26 -0
  250. package/lib/typescript/commonjs/ui/components/WidgetPanel.d.ts.map +1 -0
  251. package/lib/typescript/commonjs/ui/components/icons.d.ts +38 -0
  252. package/lib/typescript/commonjs/ui/components/icons.d.ts.map +1 -0
  253. package/lib/typescript/commonjs/ui/hooks/use-keyboard-overlap.d.ts +33 -0
  254. package/lib/typescript/commonjs/ui/hooks/use-keyboard-overlap.d.ts.map +1 -0
  255. package/lib/typescript/commonjs/ui/hooks/use-reduce-motion.d.ts +11 -0
  256. package/lib/typescript/commonjs/ui/hooks/use-reduce-motion.d.ts.map +1 -0
  257. package/lib/typescript/commonjs/ui/hooks/use-widget-insets.d.ts +16 -0
  258. package/lib/typescript/commonjs/ui/hooks/use-widget-insets.d.ts.map +1 -0
  259. package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts +11 -0
  260. package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts.map +1 -0
  261. package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts +349 -0
  262. package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts.map +1 -0
  263. package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts +48 -0
  264. package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts.map +1 -0
  265. package/lib/typescript/commonjs/ui/screens/conversations/ConversationsList.d.ts +10 -0
  266. package/lib/typescript/commonjs/ui/screens/conversations/ConversationsList.d.ts.map +1 -0
  267. package/lib/typescript/commonjs/ui/screens/conversations/ConversationsList.styles.d.ts +113 -0
  268. package/lib/typescript/commonjs/ui/screens/conversations/ConversationsList.styles.d.ts.map +1 -0
  269. package/lib/typescript/commonjs/ui/screens/conversations/ConversationsList.types.d.ts +31 -0
  270. package/lib/typescript/commonjs/ui/screens/conversations/ConversationsList.types.d.ts.map +1 -0
  271. package/lib/typescript/commonjs/ui/screens/home/HomeView.d.ts +17 -0
  272. package/lib/typescript/commonjs/ui/screens/home/HomeView.d.ts.map +1 -0
  273. package/lib/typescript/commonjs/ui/screens/home/HomeView.styles.d.ts +227 -0
  274. package/lib/typescript/commonjs/ui/screens/home/HomeView.styles.d.ts.map +1 -0
  275. package/lib/typescript/commonjs/ui/screens/home/HomeView.types.d.ts +40 -0
  276. package/lib/typescript/commonjs/ui/screens/home/HomeView.types.d.ts.map +1 -0
  277. package/lib/typescript/commonjs/ui/screens/home/greeting.d.ts +19 -0
  278. package/lib/typescript/commonjs/ui/screens/home/greeting.d.ts.map +1 -0
  279. package/lib/typescript/commonjs/ui/screens/pre-chat/PreChatForm.d.ts +7 -0
  280. package/lib/typescript/commonjs/ui/screens/pre-chat/PreChatForm.d.ts.map +1 -0
  281. package/lib/typescript/commonjs/ui/screens/pre-chat/PreChatForm.styles.d.ts +115 -0
  282. package/lib/typescript/commonjs/ui/screens/pre-chat/PreChatForm.styles.d.ts.map +1 -0
  283. package/lib/typescript/commonjs/ui/screens/pre-chat/PreChatForm.types.d.ts +20 -0
  284. package/lib/typescript/commonjs/ui/screens/pre-chat/PreChatForm.types.d.ts.map +1 -0
  285. package/lib/typescript/commonjs/ui/styles/global.d.ts +46 -0
  286. package/lib/typescript/commonjs/ui/styles/global.d.ts.map +1 -0
  287. package/lib/typescript/commonjs/ui/styles/theme.d.ts +103 -0
  288. package/lib/typescript/commonjs/ui/styles/theme.d.ts.map +1 -0
  289. package/lib/typescript/commonjs/ui/utils/conversations.d.ts +41 -0
  290. package/lib/typescript/commonjs/ui/utils/conversations.d.ts.map +1 -0
  291. package/lib/typescript/commonjs/ui/utils/html.d.ts +10 -0
  292. package/lib/typescript/commonjs/ui/utils/html.d.ts.map +1 -0
  293. package/lib/typescript/commonjs/ui/utils/native-gradient.d.ts +61 -0
  294. package/lib/typescript/commonjs/ui/utils/native-gradient.d.ts.map +1 -0
  295. package/lib/typescript/commonjs/ui/utils/rtl.d.ts +33 -0
  296. package/lib/typescript/commonjs/ui/utils/rtl.d.ts.map +1 -0
  297. package/lib/typescript/commonjs/ui/utils/safe-url.d.ts +21 -0
  298. package/lib/typescript/commonjs/ui/utils/safe-url.d.ts.map +1 -0
  299. package/lib/typescript/module/agent-ui-blocks.d.ts +20 -0
  300. package/lib/typescript/module/agent-ui-blocks.d.ts.map +1 -0
  301. package/lib/typescript/module/api/client.d.ts +115 -0
  302. package/lib/typescript/module/api/client.d.ts.map +1 -0
  303. package/lib/typescript/module/api/realtime.d.ts +39 -0
  304. package/lib/typescript/module/api/realtime.d.ts.map +1 -0
  305. package/lib/typescript/module/api/sse-parser.d.ts +13 -0
  306. package/lib/typescript/module/api/sse-parser.d.ts.map +1 -0
  307. package/lib/typescript/module/controller.d.ts +15 -0
  308. package/lib/typescript/module/controller.d.ts.map +1 -0
  309. package/lib/typescript/module/device-id.d.ts +15 -0
  310. package/lib/typescript/module/device-id.d.ts.map +1 -0
  311. package/lib/typescript/module/events.d.ts +41 -0
  312. package/lib/typescript/module/events.d.ts.map +1 -0
  313. package/lib/typescript/module/i18n.d.ts +78 -0
  314. package/lib/typescript/module/i18n.d.ts.map +1 -0
  315. package/lib/typescript/module/index.d.ts +103 -0
  316. package/lib/typescript/module/index.d.ts.map +1 -0
  317. package/lib/typescript/module/locales.d.ts +18 -0
  318. package/lib/typescript/module/locales.d.ts.map +1 -0
  319. package/lib/typescript/module/package.json +1 -0
  320. package/lib/typescript/module/push/push-notifications.d.ts +71 -0
  321. package/lib/typescript/module/push/push-notifications.d.ts.map +1 -0
  322. package/lib/typescript/module/state/use-widget-state.d.ts +10 -0
  323. package/lib/typescript/module/state/use-widget-state.d.ts.map +1 -0
  324. package/lib/typescript/module/state/widget-store.d.ts +227 -0
  325. package/lib/typescript/module/state/widget-store.d.ts.map +1 -0
  326. package/lib/typescript/module/storage.d.ts +42 -0
  327. package/lib/typescript/module/storage.d.ts.map +1 -0
  328. package/lib/typescript/module/types.d.ts +391 -0
  329. package/lib/typescript/module/types.d.ts.map +1 -0
  330. package/lib/typescript/module/ui/WidgetProvider.d.ts +15 -0
  331. package/lib/typescript/module/ui/WidgetProvider.d.ts.map +1 -0
  332. package/lib/typescript/module/ui/components/AgentUiBlocks.d.ts +27 -0
  333. package/lib/typescript/module/ui/components/AgentUiBlocks.d.ts.map +1 -0
  334. package/lib/typescript/module/ui/components/Avatar.d.ts +19 -0
  335. package/lib/typescript/module/ui/components/Avatar.d.ts.map +1 -0
  336. package/lib/typescript/module/ui/components/ConversationRow.d.ts +26 -0
  337. package/lib/typescript/module/ui/components/ConversationRow.d.ts.map +1 -0
  338. package/lib/typescript/module/ui/components/ErrorBoundary.d.ts +27 -0
  339. package/lib/typescript/module/ui/components/ErrorBoundary.d.ts.map +1 -0
  340. package/lib/typescript/module/ui/components/Gradient.d.ts +33 -0
  341. package/lib/typescript/module/ui/components/Gradient.d.ts.map +1 -0
  342. package/lib/typescript/module/ui/components/Launcher.d.ts +18 -0
  343. package/lib/typescript/module/ui/components/Launcher.d.ts.map +1 -0
  344. package/lib/typescript/module/ui/components/Markdown.d.ts +19 -0
  345. package/lib/typescript/module/ui/components/Markdown.d.ts.map +1 -0
  346. package/lib/typescript/module/ui/components/OutboundMessageCard.d.ts +20 -0
  347. package/lib/typescript/module/ui/components/OutboundMessageCard.d.ts.map +1 -0
  348. package/lib/typescript/module/ui/components/WidgetPanel.d.ts +26 -0
  349. package/lib/typescript/module/ui/components/WidgetPanel.d.ts.map +1 -0
  350. package/lib/typescript/module/ui/components/icons.d.ts +38 -0
  351. package/lib/typescript/module/ui/components/icons.d.ts.map +1 -0
  352. package/lib/typescript/module/ui/hooks/use-keyboard-overlap.d.ts +33 -0
  353. package/lib/typescript/module/ui/hooks/use-keyboard-overlap.d.ts.map +1 -0
  354. package/lib/typescript/module/ui/hooks/use-reduce-motion.d.ts +11 -0
  355. package/lib/typescript/module/ui/hooks/use-reduce-motion.d.ts.map +1 -0
  356. package/lib/typescript/module/ui/hooks/use-widget-insets.d.ts +16 -0
  357. package/lib/typescript/module/ui/hooks/use-widget-insets.d.ts.map +1 -0
  358. package/lib/typescript/module/ui/screens/chat/ChatView.d.ts +11 -0
  359. package/lib/typescript/module/ui/screens/chat/ChatView.d.ts.map +1 -0
  360. package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts +349 -0
  361. package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts.map +1 -0
  362. package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts +48 -0
  363. package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts.map +1 -0
  364. package/lib/typescript/module/ui/screens/conversations/ConversationsList.d.ts +10 -0
  365. package/lib/typescript/module/ui/screens/conversations/ConversationsList.d.ts.map +1 -0
  366. package/lib/typescript/module/ui/screens/conversations/ConversationsList.styles.d.ts +113 -0
  367. package/lib/typescript/module/ui/screens/conversations/ConversationsList.styles.d.ts.map +1 -0
  368. package/lib/typescript/module/ui/screens/conversations/ConversationsList.types.d.ts +31 -0
  369. package/lib/typescript/module/ui/screens/conversations/ConversationsList.types.d.ts.map +1 -0
  370. package/lib/typescript/module/ui/screens/home/HomeView.d.ts +17 -0
  371. package/lib/typescript/module/ui/screens/home/HomeView.d.ts.map +1 -0
  372. package/lib/typescript/module/ui/screens/home/HomeView.styles.d.ts +227 -0
  373. package/lib/typescript/module/ui/screens/home/HomeView.styles.d.ts.map +1 -0
  374. package/lib/typescript/module/ui/screens/home/HomeView.types.d.ts +40 -0
  375. package/lib/typescript/module/ui/screens/home/HomeView.types.d.ts.map +1 -0
  376. package/lib/typescript/module/ui/screens/home/greeting.d.ts +19 -0
  377. package/lib/typescript/module/ui/screens/home/greeting.d.ts.map +1 -0
  378. package/lib/typescript/module/ui/screens/pre-chat/PreChatForm.d.ts +7 -0
  379. package/lib/typescript/module/ui/screens/pre-chat/PreChatForm.d.ts.map +1 -0
  380. package/lib/typescript/module/ui/screens/pre-chat/PreChatForm.styles.d.ts +115 -0
  381. package/lib/typescript/module/ui/screens/pre-chat/PreChatForm.styles.d.ts.map +1 -0
  382. package/lib/typescript/module/ui/screens/pre-chat/PreChatForm.types.d.ts +20 -0
  383. package/lib/typescript/module/ui/screens/pre-chat/PreChatForm.types.d.ts.map +1 -0
  384. package/lib/typescript/module/ui/styles/global.d.ts +46 -0
  385. package/lib/typescript/module/ui/styles/global.d.ts.map +1 -0
  386. package/lib/typescript/module/ui/styles/theme.d.ts +103 -0
  387. package/lib/typescript/module/ui/styles/theme.d.ts.map +1 -0
  388. package/lib/typescript/module/ui/utils/conversations.d.ts +41 -0
  389. package/lib/typescript/module/ui/utils/conversations.d.ts.map +1 -0
  390. package/lib/typescript/module/ui/utils/html.d.ts +10 -0
  391. package/lib/typescript/module/ui/utils/html.d.ts.map +1 -0
  392. package/lib/typescript/module/ui/utils/native-gradient.d.ts +61 -0
  393. package/lib/typescript/module/ui/utils/native-gradient.d.ts.map +1 -0
  394. package/lib/typescript/module/ui/utils/rtl.d.ts +33 -0
  395. package/lib/typescript/module/ui/utils/rtl.d.ts.map +1 -0
  396. package/lib/typescript/module/ui/utils/safe-url.d.ts +21 -0
  397. package/lib/typescript/module/ui/utils/safe-url.d.ts.map +1 -0
  398. package/metro.js +40 -0
  399. package/package.json +116 -0
  400. package/src/agent-ui-blocks.ts +302 -0
  401. package/src/api/client.ts +332 -0
  402. package/src/api/realtime.ts +225 -0
  403. package/src/api/sse-parser.ts +51 -0
  404. package/src/controller.ts +20 -0
  405. package/src/device-id.ts +71 -0
  406. package/src/events.ts +74 -0
  407. package/src/i18n.ts +114 -0
  408. package/src/index.tsx +199 -0
  409. package/src/locales.ts +80 -0
  410. package/src/push/push-notifications.ts +254 -0
  411. package/src/state/use-widget-state.ts +20 -0
  412. package/src/state/widget-store.ts +1815 -0
  413. package/src/storage.ts +153 -0
  414. package/src/types.ts +445 -0
  415. package/src/ui/WidgetProvider.tsx +434 -0
  416. package/src/ui/components/AgentUiBlocks.tsx +912 -0
  417. package/src/ui/components/Avatar.tsx +86 -0
  418. package/src/ui/components/ConversationRow.tsx +163 -0
  419. package/src/ui/components/ErrorBoundary.tsx +49 -0
  420. package/src/ui/components/Gradient.tsx +165 -0
  421. package/src/ui/components/Launcher.tsx +128 -0
  422. package/src/ui/components/Markdown.tsx +291 -0
  423. package/src/ui/components/OutboundMessageCard.tsx +154 -0
  424. package/src/ui/components/WidgetPanel.tsx +133 -0
  425. package/src/ui/components/icons.tsx +315 -0
  426. package/src/ui/hooks/use-keyboard-overlap.ts +134 -0
  427. package/src/ui/hooks/use-reduce-motion.ts +39 -0
  428. package/src/ui/hooks/use-widget-insets.ts +44 -0
  429. package/src/ui/screens/chat/ChatView.styles.ts +357 -0
  430. package/src/ui/screens/chat/ChatView.tsx +690 -0
  431. package/src/ui/screens/chat/ChatView.types.ts +46 -0
  432. package/src/ui/screens/conversations/ConversationsList.styles.ts +111 -0
  433. package/src/ui/screens/conversations/ConversationsList.tsx +144 -0
  434. package/src/ui/screens/conversations/ConversationsList.types.ts +33 -0
  435. package/src/ui/screens/home/HomeView.styles.ts +246 -0
  436. package/src/ui/screens/home/HomeView.tsx +336 -0
  437. package/src/ui/screens/home/HomeView.types.ts +42 -0
  438. package/src/ui/screens/home/greeting.ts +50 -0
  439. package/src/ui/screens/pre-chat/PreChatForm.styles.ts +120 -0
  440. package/src/ui/screens/pre-chat/PreChatForm.tsx +221 -0
  441. package/src/ui/screens/pre-chat/PreChatForm.types.ts +22 -0
  442. package/src/ui/styles/global.ts +50 -0
  443. package/src/ui/styles/theme.ts +155 -0
  444. package/src/ui/utils/conversations.ts +122 -0
  445. package/src/ui/utils/html.ts +58 -0
  446. package/src/ui/utils/native-gradient.ts +60 -0
  447. package/src/ui/utils/rtl.ts +43 -0
  448. package/src/ui/utils/safe-url.ts +63 -0
@@ -0,0 +1,1682 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.WidgetStore = void 0;
7
+ var _reactNative = require("react-native");
8
+ var _client = require("../api/client.js");
9
+ var _realtime = require("../api/realtime.js");
10
+ var _deviceId = require("../device-id.js");
11
+ var _events = require("../events.js");
12
+ var _pushNotifications = require("../push/push-notifications.js");
13
+ var _storage = require("../storage.js");
14
+ var _types = require("../types.js");
15
+ var _safeUrl = require("../ui/utils/safe-url.js");
16
+ /**
17
+ * The widget's single source of truth.
18
+ *
19
+ * Ported from the web widget's `client/src/widget/hooks/use-widget-state.ts`
20
+ * plus the routing/tab logic in `client/src/widget/App.tsx`. Unlike the web
21
+ * version this is a plain store rather than a hook, because the SSE connection
22
+ * and the unread count must stay alive while the UI is closed — the modal is
23
+ * only a subscriber.
24
+ */
25
+
26
+ const SDK_VERSION = '@repzo/widget-react-native@1.0.0';
27
+
28
+ /** The platform this session reports — mobile sessions get the Mobile SDKs overrides. */
29
+ function sessionPlatform() {
30
+ return _reactNative.Platform.OS === 'ios' || _reactNative.Platform.OS === 'android' ? _reactNative.Platform.OS : 'web';
31
+ }
32
+
33
+ /** CONFIG_KEY entry: the session config plus where it came from. */
34
+
35
+ const TYPING_CLEAR_MS = 30_000;
36
+ const TYPING_SETTLE_MS = 3_000;
37
+ /** Visitor → agent typing pings are rate limited at 30/min server-side. */
38
+ const TYPING_PING_INTERVAL_MS = 4_000;
39
+ /** Guards against the SSE stream replaying messages after a reconnect. */
40
+ const SEEN_MESSAGE_LIMIT = 100;
41
+ /** Logout's server calls are best-effort — a dead network must not block sign-out. */
42
+ const LOGOUT_REQUEST_TIMEOUT_MS = 5_000;
43
+ /** A server that 401s every fresh stream must not spin init (10/min limit). */
44
+ const STREAM_REAUTH_MIN_INTERVAL_MS = 30_000;
45
+ /** Live replies in the open thread advance the server read cursor, batched. */
46
+ const MARK_READ_DEBOUNCE_MS = 1_000;
47
+ /** Live events held back while a draft's first send is creating its thread. */
48
+ const DRAFT_EVENT_BUFFER_LIMIT = 50;
49
+ /** Plain-http `baseUrl` is only accepted for these (plus `*.local` and `__DEV__`). */
50
+ const LOCAL_DEV_HOSTS = new Set(['localhost', '127.0.0.1', '10.0.2.2']);
51
+ const sumUnread = map => Object.values(map).reduce((sum, count) => sum + count, 0);
52
+ const INITIAL_STATE = {
53
+ booted: false,
54
+ loading: false,
55
+ error: null,
56
+ errorCode: null,
57
+ config: null,
58
+ identityId: null,
59
+ isOpen: false,
60
+ view: 'bubble',
61
+ activeTab: 'home',
62
+ conversations: [],
63
+ activeConversationId: null,
64
+ messages: [],
65
+ messagesLoading: false,
66
+ hasMoreMessages: false,
67
+ messageCursor: null,
68
+ agentTyping: false,
69
+ unreadCount: 0,
70
+ unreadByConversation: {},
71
+ locale: 'en',
72
+ prefillText: '',
73
+ outboundMessage: null,
74
+ visitorProfile: {}
75
+ };
76
+
77
+ /** Everything in state that belongs to the signed-in visitor. */
78
+ const USER_SCOPED_STATE = {
79
+ // A wiped session is not booted until the next init lands.
80
+ booted: false,
81
+ identityId: null,
82
+ conversations: [],
83
+ activeConversationId: null,
84
+ messages: [],
85
+ messagesLoading: false,
86
+ hasMoreMessages: false,
87
+ messageCursor: null,
88
+ agentTyping: false,
89
+ unreadCount: 0,
90
+ unreadByConversation: {},
91
+ prefillText: '',
92
+ outboundMessage: null,
93
+ visitorProfile: {}
94
+ };
95
+ class WidgetStore {
96
+ client = new _client.WidgetApiClient();
97
+ events = new _events.EventEmitter();
98
+ push = new _pushNotifications.PushManager(this.client, conversationId => {
99
+ void this.openConversation(conversationId);
100
+ });
101
+ state = INITIAL_STATE;
102
+ subscribers = new Set();
103
+ realtime = new _realtime.WidgetRealtime();
104
+ typingTimer = null;
105
+ pendingAgentByConversation = {};
106
+ bootConfig = null;
107
+ bootPromise = null;
108
+
109
+ /** Identity key of the in-flight boot — a boot for someone else is not deduped. */
110
+ bootingUserKey = null;
111
+ currentUser = null;
112
+
113
+ /** Who the stored token was issued to; null = anonymous. */
114
+ currentUserKey = null;
115
+
116
+ /** True once `currentUserKey` mirrors storage — memory is then authoritative. */
117
+ userKeyLoaded = false;
118
+ pendingConversationId = null;
119
+ appStateSubscription = null;
120
+ lastTypingPingAt = 0;
121
+ visitorTypingActive = false;
122
+ seenMessageIds = [];
123
+
124
+ /**
125
+ * Identity generation. Bumped whenever boot/login/logout is requested; every
126
+ * async path that writes session state captures it first and bails once it
127
+ * moved, so a slow init for user A can never land after B logged in or
128
+ * after logout.
129
+ */
130
+ epoch = 0;
131
+
132
+ /** Serializes identity ops (and 401 re-inits) — one init or wipe at a time. */
133
+ identityQueue = Promise.resolve();
134
+
135
+ /** Shared by concurrent 401s: one re-init, not one per failed request. */
136
+ reauthPromise = null;
137
+
138
+ /** Set by logout(); blocks 401 re-init and realtime until the next boot/login. */
139
+ loggedOut = false;
140
+
141
+ /**
142
+ * True during wipeSession(). A 401 from a request still on the wire (a push
143
+ * registration it waits for) must not queue a re-init behind the wipe.
144
+ */
145
+ wiping = false;
146
+
147
+ /** Set by a terminal 403 — stops realtime and re-init until a successful init. */
148
+ terminalErrorCode = null;
149
+ lastStreamReauthAt = 0;
150
+
151
+ /** Queued/running boot, login and logout ops — Retry must not cut in. */
152
+ identityOpsInFlight = 0;
153
+
154
+ /**
155
+ * Bumped whenever a different thread (or a fresh draft) takes the screen.
156
+ * Message loads capture it and drop their result once it moved, so a slow
157
+ * response for A never paints under B.
158
+ */
159
+ threadGeneration = 0;
160
+
161
+ /**
162
+ * The in-flight first send of a draft, which creates its conversation.
163
+ * Sends from the same draft wait for its id instead of creating another.
164
+ */
165
+ conversationCreation = null;
166
+
167
+ /** Live events for a thread that may be the one the draft is creating. */
168
+ draftEvents = [];
169
+
170
+ /** False until this stream's first `connected`; later ones are reconnects. */
171
+ streamConnectedOnce = false;
172
+
173
+ /** The stream errored before its first `connected` — that one is a reconnect too. */
174
+ streamErrored = false;
175
+ readTimer = null;
176
+ readConversationId = null;
177
+ constructor() {
178
+ this.client.reauthorize = failedToken => this.reauthorize(failedToken);
179
+ this.client.onTerminalError = error => this.handleTerminalError(error);
180
+ }
181
+
182
+ // ─── Subscription plumbing ──────────────────────────────────────────────
183
+
184
+ subscribe = listener => {
185
+ this.subscribers.add(listener);
186
+ return () => {
187
+ this.subscribers.delete(listener);
188
+ };
189
+ };
190
+ getState = () => this.state;
191
+ setState(patch) {
192
+ const previous = this.state;
193
+ const resolved = typeof patch === 'function' ? patch(previous) : patch;
194
+ const next = {
195
+ ...previous,
196
+ ...resolved
197
+ };
198
+ // The total is always derived from the per-thread map, so no surface can
199
+ // zero the badge while threads stay unread.
200
+ next.unreadCount = sumUnread(next.unreadByConversation);
201
+ this.state = next;
202
+ if (next.unreadCount !== previous.unreadCount) {
203
+ this.events.emit(_events.RepzoWidgetEvents.UnreadCountDidChange, {
204
+ count: next.unreadCount
205
+ });
206
+ }
207
+ if (next.isOpen !== previous.isOpen) {
208
+ this.events.emit(next.isOpen ? _events.RepzoWidgetEvents.WindowDidShow : _events.RepzoWidgetEvents.WindowDidHide, undefined);
209
+ }
210
+ this.subscribers.forEach(listener => listener());
211
+ }
212
+
213
+ // ─── Identity queue ─────────────────────────────────────────────────────
214
+
215
+ serialize(op) {
216
+ const run = this.identityQueue.then(op);
217
+ this.identityQueue = run.catch(() => {});
218
+ return run;
219
+ }
220
+
221
+ /** Queue a requested identity op; anything older becomes stale right away. */
222
+ enqueueIdentity(op) {
223
+ const epoch = ++this.epoch;
224
+ // A pending boot is superseded — a later boot() must not dedupe onto it.
225
+ this.bootPromise = null;
226
+ this.bootingUserKey = null;
227
+ this.identityOpsInFlight += 1;
228
+ return this.serialize(() => op(epoch)).finally(() => {
229
+ this.identityOpsInFlight -= 1;
230
+ });
231
+ }
232
+ isStale(epoch) {
233
+ return epoch !== this.epoch;
234
+ }
235
+
236
+ // ─── Boot / identity ────────────────────────────────────────────────────
237
+
238
+ async boot(options) {
239
+ if (!options?.channelAccountId) {
240
+ throw new _client.RepzoWidgetError('boot() requires channelAccountId', 0, 'http');
241
+ }
242
+ const baseUrl = assertSecureBaseUrl(options.baseUrl?.trim() || _types.DEFAULT_BASE_URL);
243
+ if (options.onOpenUrl !== undefined) (0, _safeUrl.setOpenUrlHandler)(options.onOpenUrl ?? null);
244
+ const sameTarget = this.bootConfig?.baseUrl === baseUrl && this.bootConfig?.channelAccountId === options.channelAccountId;
245
+ const userKey = options.user ? identityKey(options.user) : null;
246
+ if (sameTarget && this.bootPromise && (!userKey || userKey === this.bootingUserKey)) {
247
+ return this.bootPromise;
248
+ }
249
+ if (sameTarget && this.state.booted && !this.state.error && !this.userChanged(options.user)) {
250
+ return;
251
+ }
252
+ this.bootConfig = {
253
+ baseUrl,
254
+ channelAccountId: options.channelAccountId
255
+ };
256
+ this.client.configure(baseUrl, options.channelAccountId);
257
+ if (options.push) this.push.configure(options.push);
258
+
259
+ // Paint with the last known theme immediately — without this the UI
260
+ // flashes the built-in defaults (color, labels) until init returns.
261
+ void this.seedCachedConfig();
262
+ const user = options.user;
263
+ const promise = this.enqueueIdentity(async epoch => {
264
+ if (this.isStale(epoch)) return;
265
+ this.loggedOut = false;
266
+ this.currentUser = user ?? this.currentUser;
267
+ try {
268
+ await this.establishSession(user, epoch);
269
+ } catch (error) {
270
+ // Boot failures must not crash the host app — surface through state.
271
+ if (!this.isStale(epoch)) {
272
+ this.setState({
273
+ loading: false,
274
+ error: error instanceof Error ? error.message : 'Failed to load chat',
275
+ errorCode: error instanceof _client.RepzoWidgetError ? error.code : 'http'
276
+ });
277
+ }
278
+ throw error;
279
+ }
280
+ }).finally(() => {
281
+ if (this.bootPromise === promise) {
282
+ this.bootPromise = null;
283
+ this.bootingUserKey = null;
284
+ }
285
+ });
286
+ this.bootPromise = promise;
287
+ this.bootingUserKey = userKey;
288
+ return promise;
289
+ }
290
+ userChanged(user) {
291
+ if (!user) return false;
292
+ return identityKey(user) !== this.currentUserKey;
293
+ }
294
+
295
+ /**
296
+ * Init for `user` — or, when undefined, for whoever the stored token
297
+ * belongs to. The stored token is only resumed when it is anonymous or
298
+ * already belongs to `user`: resuming another identified user's token would
299
+ * hand their conversations to this one, so that case signs them out first.
300
+ */
301
+ async establishSession(user, epoch) {
302
+ let resumeToken = true;
303
+ if (user) {
304
+ const storedKey = await this.storedUserKey();
305
+ if (this.isStale(epoch)) return;
306
+ if (storedKey && storedKey !== identityKey(user)) {
307
+ await this.wipeSession();
308
+ if (this.isStale(epoch)) return;
309
+ resumeToken = false;
310
+ }
311
+ this.currentUser = user;
312
+ }
313
+ await this.runInit({
314
+ user,
315
+ resumeToken,
316
+ epoch
317
+ });
318
+ }
319
+ async runInit(params) {
320
+ this.setState({
321
+ loading: true,
322
+ error: null,
323
+ errorCode: null
324
+ });
325
+ try {
326
+ await this.initSession(params);
327
+ } catch (error) {
328
+ if (!this.isStale(params.epoch)) {
329
+ if (error instanceof _client.RepzoWidgetError && (0, _client.isTerminalErrorCode)(error.code)) {
330
+ this.handleTerminalError(error);
331
+ } else if (!this.state.booted) {
332
+ // No usable session: surface it so the UI shows Retry instead of a
333
+ // chat that looks alive. A failed silent re-init of a booted
334
+ // session stays quiet — the next request or foreground retries.
335
+ this.setState({
336
+ loading: false,
337
+ error: error instanceof Error ? error.message : 'Failed to load chat',
338
+ errorCode: error instanceof _client.RepzoWidgetError ? error.code : 'http'
339
+ });
340
+ } else {
341
+ this.setState({
342
+ loading: false
343
+ });
344
+ }
345
+ // Foreground resume is how a failed boot recovers on its own.
346
+ if (!this.loggedOut) this.watchAppState();
347
+ }
348
+ throw error;
349
+ }
350
+ }
351
+
352
+ /** Every `await` is followed by an epoch check: stale results write nothing. */
353
+ async initSession(params) {
354
+ const {
355
+ user,
356
+ epoch
357
+ } = params;
358
+ const trackingVisitorId = await (0, _deviceId.getDeviceId)();
359
+ if (this.isStale(epoch)) return;
360
+ // Read before init: a resumed identified session keeps its key.
361
+ const priorKey = user ? null : await this.storedUserKey();
362
+ if (this.isStale(epoch)) return;
363
+ const result = await this.client.init({
364
+ user,
365
+ trackingVisitorId,
366
+ platform: sessionPlatform(),
367
+ sdkVersion: SDK_VERSION,
368
+ resumeToken: params.resumeToken
369
+ });
370
+ if (this.isStale(epoch)) return;
371
+
372
+ // Only a session the server actually identified (init echoes `user`) is
373
+ // keyed to a user; anything else is anonymous and safe to resume later.
374
+ let userKey = null;
375
+ if (result.user) {
376
+ if (user) userKey = identityKey(user);else userKey = priorKey ?? (result.user.email ? identityKey({
377
+ email: result.user.email
378
+ }) : null);
379
+ }
380
+ this.currentUserKey = userKey;
381
+ this.userKeyLoaded = true;
382
+ this.terminalErrorCode = null;
383
+ // Token and user key land together, before the next stale check — a
384
+ // queued op must never see one without the other.
385
+ await Promise.all([this.client.setToken(result.token), userKey ? _storage.SecureStorage.setItem(_storage.USER_KEY, userKey) : _storage.SecureStorage.removeItem(_storage.USER_KEY)]);
386
+ if (this.isStale(epoch)) return;
387
+ const conversations = result.conversations.map(conversation => this.hydrateConversation(conversation));
388
+
389
+ // Server counts win (they include replies pushed while the app was
390
+ // killed); the locally persisted map only covers older Workstation
391
+ // deployments that don't send unreadCount yet.
392
+ const storedUnread = await this.loadUnread();
393
+ if (this.isStale(epoch)) return;
394
+ const unreadByConversation = {};
395
+ for (const conversation of conversations) {
396
+ const count = typeof conversation.unreadCount === 'number' ? conversation.unreadCount : storedUnread[conversation.id] ?? 0;
397
+ if (count > 0) unreadByConversation[conversation.id] = count;
398
+ }
399
+ // A silent re-init can land while a thread is on screen — never badge it.
400
+ const onScreenId = this.state.activeConversationId;
401
+ if (onScreenId && this.isThreadOnScreen(onScreenId)) delete unreadByConversation[onScreenId];
402
+ this.persistUnread(unreadByConversation);
403
+ this.setState(current => ({
404
+ booted: true,
405
+ loading: false,
406
+ error: null,
407
+ errorCode: null,
408
+ config: result.config,
409
+ identityId: result.identityId,
410
+ conversations,
411
+ // Also seeds the launcher badge, so a reply that landed while the app
412
+ // was closed still shows on the host's icon.
413
+ unreadByConversation,
414
+ locale: current.locale === INITIAL_STATE.locale ? result.config.defaultLocale : current.locale,
415
+ activeTab: firstEnabledSpace(result.config?.spaces) ?? current.activeTab,
416
+ visitorProfile: {
417
+ ...current.visitorProfile,
418
+ ...(user?.name ? {
419
+ name: user.name
420
+ } : {}),
421
+ ...(user?.email ? {
422
+ email: user.email
423
+ } : {}),
424
+ ...(user?.phone ? {
425
+ phone: user.phone
426
+ } : {}),
427
+ // Init echoes back the identity the backend holds for this visitor.
428
+ // It is the authority once identity verification has run, so it wins
429
+ // over whatever the host passed in — and it is the only source when
430
+ // the host boots without user attributes at all.
431
+ ...(result.user?.name ? {
432
+ name: result.user.name
433
+ } : {}),
434
+ ...(result.user?.email ? {
435
+ email: result.user.email
436
+ } : {})
437
+ }
438
+ }));
439
+ this.persistConfig(result.config);
440
+ this.connectRealtime(result.token);
441
+ this.watchAppState();
442
+ this.loadOutboundMessage();
443
+ // Re-runs on login switches too — the token moves to the new identity.
444
+ void this.push.start();
445
+ if (this.pendingConversationId) {
446
+ const conversationId = this.pendingConversationId;
447
+ this.pendingConversationId = null;
448
+ void this.openConversation(conversationId);
449
+ } else {
450
+ // A re-init after a 401 means the stream was down: catch the open
451
+ // thread up (a no-op when nothing is on screen).
452
+ void this.refreshOpenThread();
453
+ }
454
+ }
455
+
456
+ /**
457
+ * The last config this device received, applied before init returns so the
458
+ * UI opens with the workspace's real theme instead of the built-in defaults.
459
+ * Skipped once a fresh config is in state — never downgrades live data.
460
+ */
461
+ async seedCachedConfig() {
462
+ try {
463
+ void _storage.Storage.removeItem(_storage.LEGACY_CONFIG_KEY);
464
+ const raw = await _storage.Storage.getItem(_storage.CONFIG_KEY);
465
+ if (!raw || this.state.config) return;
466
+ const cached = JSON.parse(raw);
467
+ // Only a config cached by a session init for this same widget, server
468
+ // and platform is painted — anything else (another channel, the web
469
+ // design) would flash before init replaces it, so the loading state
470
+ // shows instead.
471
+ if (cached?.v === 2 && cached.config && typeof cached.config === 'object' && cached.baseUrl === this.bootConfig?.baseUrl && cached.channelAccountId === this.bootConfig?.channelAccountId && cached.platform === sessionPlatform() && !this.state.config) {
472
+ this.setState({
473
+ config: cached.config
474
+ });
475
+ }
476
+ } catch {
477
+ // Corrupt cache — the network config overwrites it shortly.
478
+ }
479
+ }
480
+ persistConfig(config) {
481
+ if (!this.bootConfig) return;
482
+ const entry = {
483
+ v: 2,
484
+ baseUrl: this.bootConfig.baseUrl,
485
+ channelAccountId: this.bootConfig.channelAccountId,
486
+ platform: sessionPlatform(),
487
+ config
488
+ };
489
+ try {
490
+ void _storage.Storage.setItem(_storage.CONFIG_KEY, JSON.stringify(entry));
491
+ } catch {
492
+ // Cache miss next launch at worst.
493
+ }
494
+ }
495
+ async storedUserKey() {
496
+ if (this.userKeyLoaded) return this.currentUserKey;
497
+ const stored = await _storage.SecureStorage.getItem(_storage.USER_KEY);
498
+ // Another op may have written the key while storage was being read.
499
+ if (this.userKeyLoaded) return this.currentUserKey;
500
+ this.currentUserKey = stored || null;
501
+ this.userKeyLoaded = true;
502
+ return this.currentUserKey;
503
+ }
504
+
505
+ /**
506
+ * Called by the API client after a 401. Single-flight: concurrent 401s share
507
+ * one re-init. Runs in the identity queue but does not bump the epoch, so it
508
+ * can't cancel a boot/login queued behind it — and it gives up if one was.
509
+ */
510
+ reauthorize(failedToken) {
511
+ if (this.loggedOut || this.wiping || this.terminalErrorCode || !this.bootConfig) {
512
+ return Promise.resolve(null);
513
+ }
514
+ if (this.reauthPromise) return this.reauthPromise;
515
+ const epoch = this.epoch;
516
+ const promise = this.serialize(async () => {
517
+ if (this.loggedOut || this.terminalErrorCode || this.isStale(epoch)) return null;
518
+ // Someone already replaced the token that failed — just retry with it.
519
+ const current = await this.client.getToken();
520
+ if (this.isStale(epoch)) return null;
521
+ if (current && current !== failedToken) return current;
522
+ try {
523
+ await this.runInit({
524
+ user: this.currentUser ?? undefined,
525
+ resumeToken: false,
526
+ epoch
527
+ });
528
+ } catch {
529
+ return null;
530
+ }
531
+ if (this.isStale(epoch)) return null;
532
+ return this.client.getToken();
533
+ }).finally(() => {
534
+ if (this.reauthPromise === promise) this.reauthPromise = null;
535
+ });
536
+ this.reauthPromise = promise;
537
+ return promise;
538
+ }
539
+
540
+ /** A terminal 403 (`messenger_api_disabled`, `identity_verification_failed`). */
541
+ handleTerminalError(error) {
542
+ this.terminalErrorCode = error.code;
543
+ this.realtime.close();
544
+ this.setState({
545
+ loading: false,
546
+ error: error.message,
547
+ errorCode: error.code
548
+ });
549
+ }
550
+ async loginUnidentifiedUser() {
551
+ this.requireBootConfig();
552
+ return this.enqueueIdentity(async epoch => {
553
+ if (this.isStale(epoch)) return;
554
+ const storedKey = await this.storedUserKey();
555
+ if (this.isStale(epoch)) return;
556
+ if (this.state.booted && !this.state.error && !storedKey) return;
557
+ this.loggedOut = false;
558
+ this.currentUser = null;
559
+ // A signed-in user is signed out properly; an anonymous session is
560
+ // just dropped for a fresh one.
561
+ if (storedKey) await this.wipeSession();else await this.client.clearToken();
562
+ if (this.isStale(epoch)) return;
563
+ await this.runInit({
564
+ user: undefined,
565
+ resumeToken: false,
566
+ epoch
567
+ });
568
+ });
569
+ }
570
+ async loginUserWithUserAttributes(user) {
571
+ this.requireBootConfig();
572
+ if (!user?.userId && !user?.email) {
573
+ throw new _client.RepzoWidgetError('loginUserWithUserAttributes requires userId or email', 0, 'http');
574
+ }
575
+ return this.enqueueIdentity(async epoch => {
576
+ if (this.isStale(epoch)) return;
577
+ this.loggedOut = false;
578
+ const key = identityKey(user);
579
+ const storedKey = await this.storedUserKey();
580
+ if (this.isStale(epoch)) return;
581
+
582
+ // Same person as the resumed session — the token already points at their
583
+ // identity, so skip the (rate-limited) init round trip.
584
+ if (this.state.booted && !this.state.error && storedKey === key) {
585
+ this.currentUser = user;
586
+ this.setState(current => ({
587
+ visitorProfile: {
588
+ ...current.visitorProfile,
589
+ ...(user.name ? {
590
+ name: user.name
591
+ } : {}),
592
+ ...(user.email ? {
593
+ email: user.email
594
+ } : {}),
595
+ ...(user.phone ? {
596
+ phone: user.phone
597
+ } : {})
598
+ }
599
+ }));
600
+ return;
601
+ }
602
+ await this.establishSession(user, epoch);
603
+ });
604
+ }
605
+ async updateUser(attributes) {
606
+ this.requireBootConfig();
607
+ if (!this.state.booted) await this.bootPromise;
608
+ const customAttributes = sanitizeAttributes(attributes);
609
+ if (Object.keys(customAttributes).length === 0) return;
610
+ await this.client.identify({
611
+ customAttributes
612
+ });
613
+ }
614
+
615
+ /**
616
+ * Signs the visitor out for good — hosts should await this on sign-out.
617
+ * Synchronously stops realtime, re-init and the UI; then (queued behind any
618
+ * in-flight init) wipes the session and tells the server.
619
+ */
620
+ async logout() {
621
+ this.loggedOut = true;
622
+ this.currentUser = null;
623
+ this.pendingConversationId = null;
624
+ this.realtime.close();
625
+ this.clearTypingTimer();
626
+ this.resetThreadTracking();
627
+ this.appStateSubscription?.remove();
628
+ this.appStateSubscription = null;
629
+ this.setState({
630
+ ...INITIAL_STATE
631
+ });
632
+ await this.enqueueIdentity(() => this.wipeSession());
633
+ }
634
+
635
+ /**
636
+ * Full local + server sign-out of the stored session, used by logout() and
637
+ * by a login that switches to a different user. The local wipe runs first,
638
+ * so nothing of the old user stays readable even if the network hangs;
639
+ * then the push registration is removed and the token revoked — both
640
+ * authenticated with the captured token, time-boxed, and never re-initing.
641
+ * Only the cached channel theme survives (it carries no visitor data).
642
+ */
643
+ async wipeSession() {
644
+ this.wiping = true;
645
+ try {
646
+ await this.wipeSessionUnguarded();
647
+ } finally {
648
+ this.wiping = false;
649
+ }
650
+ }
651
+ async wipeSessionUnguarded() {
652
+ this.realtime.close();
653
+ this.clearTypingTimer();
654
+ this.resetThreadTracking();
655
+ this.appStateSubscription?.remove();
656
+ this.appStateSubscription = null;
657
+ this.currentUserKey = null;
658
+ this.userKeyLoaded = true;
659
+ this.pendingAgentByConversation = {};
660
+ this.seenMessageIds = [];
661
+ this.visitorTypingActive = false;
662
+ this.lastTypingPingAt = 0;
663
+ this.setState({
664
+ ...USER_SCOPED_STATE
665
+ });
666
+ const token = await this.client.getToken();
667
+ await Promise.all([this.client.clearToken(), _storage.SecureStorage.removeItem(_storage.USER_KEY), _storage.Storage.removeItem(_storage.UNREAD_KEY),
668
+ // A new anonymous visitor on this device gets a new tracking identity.
669
+ (0, _deviceId.rotateDeviceId)()]);
670
+
671
+ // Push DELETE first — it authenticates with the token about to be revoked.
672
+ await settleWithin(this.push.unregister(token), LOGOUT_REQUEST_TIMEOUT_MS);
673
+ if (token) {
674
+ await settleWithin(this.client.logout(token).catch(error => {
675
+ console.warn('[repzo-widget] server logout failed', error);
676
+ }), LOGOUT_REQUEST_TIMEOUT_MS);
677
+ }
678
+ }
679
+
680
+ /**
681
+ * Re-runs the last boot after a failure — wired to the error screen's retry
682
+ * and to foreground resume. Never rejects: the outcome lands in state.
683
+ */
684
+ async retry() {
685
+ if (!this.bootConfig || this.bootPromise || this.identityOpsInFlight > 0 || this.loggedOut) {
686
+ return;
687
+ }
688
+ try {
689
+ await this.boot({
690
+ ...this.bootConfig,
691
+ user: this.currentUser ?? undefined
692
+ });
693
+ } catch {
694
+ // Already surfaced through `error` / `errorCode`.
695
+ }
696
+ }
697
+
698
+ /** Drops every per-thread tracker — the visitor (or their session) changed. */
699
+ resetThreadTracking() {
700
+ this.threadGeneration += 1;
701
+ this.conversationCreation = null;
702
+ this.draftEvents = [];
703
+ this.cancelMarkRead();
704
+ }
705
+ requireBootConfig() {
706
+ if (!this.bootConfig) {
707
+ throw new _client.RepzoWidgetError('RepzoWidget.boot() must be called first', 0, 'http');
708
+ }
709
+ }
710
+
711
+ // ─── Realtime ───────────────────────────────────────────────────────────
712
+
713
+ connectRealtime(token) {
714
+ if (!this.bootConfig || this.loggedOut || this.terminalErrorCode) return;
715
+ // Whoever connects explicitly (init, foreground) refreshes on its own;
716
+ // only the stream's backoff reconnects need the catch-up.
717
+ this.streamConnectedOnce = false;
718
+ this.streamErrored = false;
719
+ this.realtime.connect(this.bootConfig.baseUrl, token, (event, data) => {
720
+ this.handleRealtimeEvent(event, data);
721
+ }, {
722
+ onUnauthorized: failedToken => {
723
+ void this.reauthorizeStream(failedToken);
724
+ },
725
+ onForbidden: error => this.handleTerminalError(error)
726
+ });
727
+ }
728
+
729
+ /** The stream got a 401: share the single-flight re-init, then reconnect. */
730
+ async reauthorizeStream(failedToken) {
731
+ const now = Date.now();
732
+ if (now - this.lastStreamReauthAt < STREAM_REAUTH_MIN_INTERVAL_MS) return;
733
+ this.lastStreamReauthAt = now;
734
+ const epoch = this.epoch;
735
+ const token = await this.reauthorize(failedToken);
736
+ // A fresh init already reconnected; this covers a token another caller
737
+ // minted just before the stream noticed.
738
+ if (token && !this.isStale(epoch) && !this.realtime.isConnected) {
739
+ this.connectRealtime(token);
740
+ }
741
+ }
742
+ watchAppState() {
743
+ if (this.appStateSubscription) return;
744
+ this.appStateSubscription = _reactNative.AppState.addEventListener('change', status => {
745
+ if (status === 'active') {
746
+ void this.resumeFromBackground();
747
+ } else if (status === 'background') {
748
+ this.realtime.close();
749
+ }
750
+ });
751
+ }
752
+ async resumeFromBackground() {
753
+ if (this.loggedOut || this.terminalErrorCode) return;
754
+ if (!this.state.booted) {
755
+ // A boot that failed (offline at launch, say) retries once the visitor
756
+ // is back — Retry stays available too.
757
+ if (this.state.error) await this.retry();
758
+ return;
759
+ }
760
+ const epoch = this.epoch;
761
+ const token = await this.client.getToken();
762
+ if (!token || this.isStale(epoch)) return;
763
+ this.connectRealtime(token);
764
+ // The stream had a gap: refresh the list so unread/snippets are correct,
765
+ // and the thread on screen so replies sent meanwhile show up.
766
+ await Promise.all([this.refreshConversations(), this.refreshOpenThread()]);
767
+ await this.refreshConfig();
768
+ }
769
+
770
+ /**
771
+ * Re-read the config so availability and copy do not go stale. `isOnline` is
772
+ * otherwise a snapshot from boot, which a long-lived app would keep showing
773
+ * hours after the team went offline.
774
+ */
775
+ async refreshConfig() {
776
+ if (!this.state.booted) return;
777
+ const epoch = this.epoch;
778
+ try {
779
+ const fresh = await this.client.getConfig(sessionPlatform());
780
+ if (!fresh || this.isStale(epoch) || !this.state.config) return;
781
+ // Only availability is refreshed here. The public config endpoint knows
782
+ // nothing about the visitor (identified vs anonymous spaces, launcher
783
+ // rules), so replacing the whole session config with it — as this used
784
+ // to — swapped the mobile design for the web one until the next init.
785
+ const config = {
786
+ ...this.state.config,
787
+ isOnline: fresh.isOnline,
788
+ ...(typeof fresh.offlineMessage === 'string' ? {
789
+ offlineMessage: fresh.offlineMessage
790
+ } : {})
791
+ };
792
+ this.setState({
793
+ config
794
+ });
795
+ this.persistConfig(config);
796
+ } catch {
797
+ // Availability stays as-is; it refreshes again on the next foreground.
798
+ }
799
+ }
800
+ handleRealtimeEvent(event, data) {
801
+ if ((event === 'message.new' || event === 'conversation.event') && this.shouldHoldForDraft(data?.conversationId)) {
802
+ this.draftEvents.push({
803
+ event,
804
+ data
805
+ });
806
+ return;
807
+ }
808
+ switch (event) {
809
+ case 'connected':
810
+ // The stream reconnected after a drop: anything sent while it
811
+ // was down never streamed, so re-read the list and the open thread.
812
+ if (this.streamConnectedOnce || this.streamErrored) {
813
+ void this.refreshConversations();
814
+ void this.refreshOpenThread();
815
+ }
816
+ this.streamConnectedOnce = true;
817
+ this.streamErrored = false;
818
+ break;
819
+ case 'error':
820
+ this.streamErrored = true;
821
+ break;
822
+ case 'message.new':
823
+ this.handleNewMessage(data);
824
+ break;
825
+ case 'conversation.updated':
826
+ this.rememberConversationAgent(data);
827
+ this.setState(current => ({
828
+ conversations: current.conversations.map(conversation => conversation.id === data?.conversationId ? {
829
+ ...conversation,
830
+ agentName: data.agentName === undefined ? conversation.agentName : data.agentName,
831
+ agentAvatar: data.agentAvatar === undefined ? conversation.agentAvatar : this.client.resolveUrl(data.agentAvatar)
832
+ } : conversation)
833
+ }));
834
+ break;
835
+ case 'conversation.event':
836
+ this.handleConversationEvent(data);
837
+ break;
838
+ case 'typing.start':
839
+ this.rememberConversationAgent(data);
840
+ if (this.isThreadOnScreen(data?.conversationId)) {
841
+ this.setState(current => ({
842
+ agentTyping: true,
843
+ conversations: current.conversations.map(conversation => conversation.id === data.conversationId ? this.applyConversationAgent(conversation) : conversation)
844
+ }));
845
+ this.scheduleTypingClear(TYPING_CLEAR_MS);
846
+ }
847
+ break;
848
+ case 'typing.stop':
849
+ if (this.isThreadOnScreen(data?.conversationId)) {
850
+ this.scheduleTypingClear(TYPING_SETTLE_MS);
851
+ }
852
+ break;
853
+ default:
854
+ break;
855
+ }
856
+ }
857
+
858
+ /**
859
+ * The thread the visitor is looking at right now — the only one live
860
+ * messages, typing and events may paint into. The `isOpen` guard matters on
861
+ * mobile: the widget spends most of its life closed, and a stale
862
+ * `view === 'chat'` would otherwise swallow the unread increment. A blank
863
+ * draft matches nothing; its thread is adopted once the first send answers.
864
+ */
865
+ isThreadOnScreen(conversationId) {
866
+ if (!conversationId) return false;
867
+ return this.state.isOpen && this.state.view === 'chat' && this.state.activeConversationId === conversationId;
868
+ }
869
+
870
+ /**
871
+ * While a draft's first send is in flight its conversation id is unknown,
872
+ * and the server may already stream the thread's first reply (or an "agent
873
+ * joined" event). Events for a thread this device doesn't know are held
874
+ * until the send answers, then replayed: into the draft if it is that
875
+ * thread, otherwise as ordinary unread traffic.
876
+ */
877
+ shouldHoldForDraft(conversationId) {
878
+ const creation = this.conversationCreation;
879
+ return !!conversationId && !!creation && creation.generation === this.threadGeneration && !this.state.activeConversationId && !this.state.conversations.some(conversation => conversation.id === conversationId) && this.draftEvents.length < DRAFT_EVENT_BUFFER_LIMIT;
880
+ }
881
+ replayDraftEvents() {
882
+ const held = this.draftEvents;
883
+ this.draftEvents = [];
884
+ held.forEach(({
885
+ event,
886
+ data
887
+ }) => this.handleRealtimeEvent(event, data));
888
+ }
889
+ persistUnread(map) {
890
+ void _storage.Storage.setItem(_storage.UNREAD_KEY, JSON.stringify(map));
891
+ }
892
+
893
+ /** Survives a relaunch so a reply received overnight is still marked unread. */
894
+ async loadUnread() {
895
+ const raw = await _storage.Storage.getItem(_storage.UNREAD_KEY);
896
+ if (!raw) return {};
897
+ try {
898
+ const parsed = JSON.parse(raw);
899
+ if (!parsed || typeof parsed !== 'object') return {};
900
+ const result = {};
901
+ for (const [id, count] of Object.entries(parsed)) {
902
+ if (typeof count === 'number' && count > 0) result[id] = count;
903
+ }
904
+ return result;
905
+ } catch {
906
+ return {};
907
+ }
908
+ }
909
+ clearUnreadFor(conversationId) {
910
+ if (!this.state.unreadByConversation[conversationId]) {
911
+ return this.state.unreadByConversation;
912
+ }
913
+ const next = {
914
+ ...this.state.unreadByConversation
915
+ };
916
+ delete next[conversationId];
917
+ this.persistUnread(next);
918
+ return next;
919
+ }
920
+
921
+ // Real server ids only: the synthetic `sse-<now>` fallback is never a replay.
922
+ isReplayedMessage(messageId) {
923
+ if (!messageId) return false;
924
+ if (this.seenMessageIds.includes(messageId)) return true;
925
+ this.seenMessageIds.push(messageId);
926
+ if (this.seenMessageIds.length > SEEN_MESSAGE_LIMIT) this.seenMessageIds.shift();
927
+ return false;
928
+ }
929
+ handleNewMessage(data) {
930
+ if (this.isReplayedMessage(data?.messageId)) return;
931
+
932
+ // The server id is what history pages and catch-up fetches carry, so it
933
+ // is what dedupes; the synthetic id only covers an older server.
934
+ const messageId = data?.messageId || `sse-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
935
+ const message = {
936
+ id: messageId,
937
+ messageType: data?.messageType || 'message',
938
+ direction: data?.direction || 'outbound',
939
+ senderType: data?.senderType || 'agent',
940
+ senderName: data?.senderName || null,
941
+ body: data?.body || null,
942
+ bodyPlain: data?.bodyPlain || null,
943
+ attachments: this.hydrateAttachments(data?.attachments, messageId),
944
+ metadata: data?.metadata || null,
945
+ status: 'delivered',
946
+ createdAt: data?.createdAt || new Date().toISOString()
947
+ };
948
+ const clearsTyping = message.direction === 'outbound' && message.senderType !== 'visitor';
949
+ if (clearsTyping) this.clearTypingTimer();
950
+ const conversationId = data?.conversationId;
951
+ // Only real replies from the team count: never the visitor's own echo,
952
+ // never system rows like "agent joined" (the server counts the same way).
953
+ const isTeamReply = message.messageType === 'message' && message.direction !== 'inbound' && message.senderType !== 'visitor';
954
+ const onScreen = this.isThreadOnScreen(conversationId);
955
+
956
+ // Set inside the updater, read after: `setState` runs synchronously.
957
+ let countedAsUnread = false;
958
+ this.setState(current => {
959
+ const conversations = conversationId ? current.conversations.map(conversation => conversation.id === conversationId ? {
960
+ ...conversation,
961
+ snippet: message.messageType === 'message' ? message.bodyPlain || conversation.snippet : conversation.snippet,
962
+ lastMessageAt: message.createdAt,
963
+ messageCount: conversation.messageCount + 1
964
+ } : conversation) : current.conversations;
965
+ if (onScreen) {
966
+ const alreadyPresent = current.messages.some(existing => existing.id === message.id);
967
+ return {
968
+ conversations,
969
+ messages: alreadyPresent ? current.messages : [...current.messages, message],
970
+ agentTyping: clearsTyping ? false : current.agentTyping
971
+ };
972
+ }
973
+ if (!isTeamReply || !conversationId) return {
974
+ conversations
975
+ };
976
+ const unreadByConversation = {
977
+ ...current.unreadByConversation,
978
+ [conversationId]: (current.unreadByConversation[conversationId] ?? 0) + 1
979
+ };
980
+ this.persistUnread(unreadByConversation);
981
+ countedAsUnread = true;
982
+ return {
983
+ conversations,
984
+ unreadByConversation
985
+ };
986
+ });
987
+
988
+ // The visitor is reading it live — move the server cursor too, or the
989
+ // next list/init would badge a reply they already saw.
990
+ if (onScreen && isTeamReply && conversationId) this.scheduleMarkRead(conversationId);
991
+
992
+ // Emitted after the state write so a listener reading the unread count sees
993
+ // the new value. Only unread-counting replies fire it, which keeps the event
994
+ // in step with the badge: never the visitor's own echo, never a system row,
995
+ // never the open thread.
996
+ if (countedAsUnread && conversationId) {
997
+ this.events.emit(_events.RepzoWidgetEvents.MessageDidArrive, {
998
+ conversationId,
999
+ message,
1000
+ conversation: this.state.conversations.find(item => item.id === conversationId) ?? null
1001
+ });
1002
+ }
1003
+
1004
+ // A reply can arrive for a conversation this device has never listed.
1005
+ if (conversationId && !this.state.conversations.some(item => item.id === conversationId)) {
1006
+ void this.refreshConversations();
1007
+ }
1008
+ }
1009
+
1010
+ /** Trailing debounce: one read call per burst of live replies. */
1011
+ scheduleMarkRead(conversationId) {
1012
+ if (this.readTimer && this.readConversationId !== conversationId) this.flushMarkRead();
1013
+ this.readConversationId = conversationId;
1014
+ if (this.readTimer) clearTimeout(this.readTimer);
1015
+ this.readTimer = setTimeout(() => this.flushMarkRead(), MARK_READ_DEBOUNCE_MS);
1016
+ }
1017
+
1018
+ /** Best effort and never re-inits — a missed cursor move only costs a badge. */
1019
+ flushMarkRead() {
1020
+ const conversationId = this.readConversationId;
1021
+ this.cancelMarkRead();
1022
+ if (!conversationId || !this.state.booted || this.loggedOut || this.terminalErrorCode) return;
1023
+ this.client.markConversationRead(conversationId).catch(() => {});
1024
+ }
1025
+ cancelMarkRead() {
1026
+ if (this.readTimer) clearTimeout(this.readTimer);
1027
+ this.readTimer = null;
1028
+ this.readConversationId = null;
1029
+ }
1030
+ handleConversationEvent(data) {
1031
+ if (!data?.conversationId) return;
1032
+ const message = {
1033
+ id: data.eventId || `event-${data.conversationId}-${Date.now()}`,
1034
+ messageType: 'system',
1035
+ direction: 'outbound',
1036
+ senderType: 'system',
1037
+ senderName: null,
1038
+ body: null,
1039
+ bodyPlain: data.bodyPlain || 'Conversation updated',
1040
+ attachments: null,
1041
+ metadata: data.metadata || null,
1042
+ status: 'delivered',
1043
+ createdAt: data.createdAt || new Date().toISOString()
1044
+ };
1045
+ if (!this.isThreadOnScreen(data.conversationId)) return;
1046
+ this.setState(current => current.messages.some(existing => existing.id === message.id) ? {} : {
1047
+ messages: [...current.messages, message]
1048
+ });
1049
+ }
1050
+ rememberConversationAgent(data) {
1051
+ if (!data?.conversationId) return;
1052
+ if (data.agentName === undefined && data.agentAvatar === undefined) return;
1053
+ this.pendingAgentByConversation[data.conversationId] = {
1054
+ agentName: data.agentName ?? null,
1055
+ agentAvatar: this.client.resolveUrl(data.agentAvatar ?? null)
1056
+ };
1057
+ }
1058
+ applyConversationAgent(conversation) {
1059
+ const pending = this.pendingAgentByConversation[conversation.id];
1060
+ if (!pending) return conversation;
1061
+ return {
1062
+ ...conversation,
1063
+ agentName: pending.agentName ?? conversation.agentName,
1064
+ agentAvatar: pending.agentAvatar ?? conversation.agentAvatar
1065
+ };
1066
+ }
1067
+ hydrateConversation(conversation) {
1068
+ return this.applyConversationAgent({
1069
+ ...conversation,
1070
+ agentAvatar: this.client.resolveUrl(conversation.agentAvatar)
1071
+ });
1072
+ }
1073
+
1074
+ /**
1075
+ * Normalizes attachments from history pages and live payloads alike. Live
1076
+ * payloads may omit `id`/`size`, so the list still gets a stable key.
1077
+ */
1078
+ hydrateAttachments(attachments, messageId) {
1079
+ if (!Array.isArray(attachments) || attachments.length === 0) return null;
1080
+ const hydrated = attachments.filter(item => !!item && typeof item === 'object').map((attachment, index) => ({
1081
+ ...attachment,
1082
+ id: typeof attachment.id === 'string' || typeof attachment.id === 'number' ? String(attachment.id) : `${messageId ?? 'attachment'}-${index}`,
1083
+ name: typeof attachment.name === 'string' && attachment.name ? attachment.name : 'Attachment',
1084
+ contentType: typeof attachment.contentType === 'string' ? attachment.contentType : '',
1085
+ size: typeof attachment.size === 'number' ? attachment.size : 0,
1086
+ // Empty (never the raw value) when the URL isn't http(s) — the preview
1087
+ // then renders the name only, with nothing to open.
1088
+ url: this.client.resolveUrl(typeof attachment.url === 'string' ? attachment.url : null) ?? ''
1089
+ }));
1090
+ return hydrated.length > 0 ? hydrated : null;
1091
+ }
1092
+ clearTypingTimer() {
1093
+ if (this.typingTimer) {
1094
+ clearTimeout(this.typingTimer);
1095
+ this.typingTimer = null;
1096
+ }
1097
+ if (this.state.agentTyping) this.setState({
1098
+ agentTyping: false
1099
+ });
1100
+ }
1101
+ scheduleTypingClear(delayMs) {
1102
+ if (this.typingTimer) clearTimeout(this.typingTimer);
1103
+ this.typingTimer = setTimeout(() => {
1104
+ this.typingTimer = null;
1105
+ this.setState({
1106
+ agentTyping: false
1107
+ });
1108
+ }, delayMs);
1109
+ }
1110
+
1111
+ // ─── UI commands ────────────────────────────────────────────────────────
1112
+
1113
+ resolveOpenView(space) {
1114
+ const {
1115
+ config,
1116
+ conversations
1117
+ } = this.state;
1118
+ const launchDirectly = config?.launchDirectly ?? false;
1119
+ const enabled = (config?.spaces ?? []).filter(item => item.enabled);
1120
+ const target = space ?? (launchDirectly ? 'messages' : firstEnabledSpace(config?.spaces) ?? 'home');
1121
+ if (target === 'messages') {
1122
+ if (conversations.length > 0) return 'conversations';
1123
+ return config?.allowNewConversations === false ? 'conversations' : 'chat';
1124
+ }
1125
+ if (enabled.length > 0 && !enabled.some(item => item.type === 'home')) return 'conversations';
1126
+ return 'home';
1127
+ }
1128
+ present(space) {
1129
+ if (!this.bootConfig) {
1130
+ console.warn('[repzo-widget] present() ignored — RepzoWidget.boot() has not been called');
1131
+ return;
1132
+ }
1133
+ const view = this.resolveOpenView(space);
1134
+ // A fresh "start a conversation" landing must not reuse an old thread.
1135
+ if (view === 'chat') this.threadGeneration += 1;
1136
+ this.setState({
1137
+ isOpen: true,
1138
+ view,
1139
+ activeTab: view === 'home' ? 'home' : 'messages',
1140
+ ...(view === 'chat' ? {
1141
+ activeConversationId: null,
1142
+ messages: [],
1143
+ messagesLoading: false,
1144
+ hasMoreMessages: false,
1145
+ messageCursor: null
1146
+ } : {})
1147
+ });
1148
+ this.track('widget_opened');
1149
+ }
1150
+ hide() {
1151
+ if (!this.state.isOpen) return;
1152
+ this.setState({
1153
+ isOpen: false,
1154
+ view: 'bubble',
1155
+ agentTyping: false
1156
+ });
1157
+ this.clearTypingTimer();
1158
+ this.track('widget_closed');
1159
+ }
1160
+ toggle(space) {
1161
+ if (this.state.isOpen) this.hide();else this.present(space);
1162
+ }
1163
+ setView(view) {
1164
+ this.setState({
1165
+ view,
1166
+ ...(view === 'chat' || view === 'conversations' ? {
1167
+ activeTab: 'messages'
1168
+ } : {}),
1169
+ ...(view === 'home' ? {
1170
+ activeTab: 'home'
1171
+ } : {})
1172
+ });
1173
+ }
1174
+ setActiveTab(tab) {
1175
+ if (tab === 'home') {
1176
+ this.setState({
1177
+ activeTab: 'home',
1178
+ view: 'home'
1179
+ });
1180
+ return;
1181
+ }
1182
+ if (this.state.conversations.length > 0) {
1183
+ this.setState({
1184
+ activeTab: 'messages',
1185
+ view: 'conversations'
1186
+ });
1187
+ } else if (this.state.config?.allowNewConversations === false) {
1188
+ this.setState({
1189
+ activeTab: 'messages',
1190
+ view: 'conversations'
1191
+ });
1192
+ } else {
1193
+ this.startNewConversation();
1194
+ }
1195
+ }
1196
+ setLocale(locale) {
1197
+ this.setState({
1198
+ locale
1199
+ });
1200
+ }
1201
+ startNewConversation(prefill) {
1202
+ this.threadGeneration += 1;
1203
+ this.setState(current => ({
1204
+ isOpen: true,
1205
+ activeTab: 'messages',
1206
+ activeConversationId: null,
1207
+ messages: [],
1208
+ // A load still in flight for the previous thread is dropped on arrival.
1209
+ messagesLoading: false,
1210
+ hasMoreMessages: false,
1211
+ messageCursor: null,
1212
+ agentTyping: false,
1213
+ view: current.config?.preChatEnabled ? 'prechat' : 'chat',
1214
+ prefillText: prefill ?? ''
1215
+ }));
1216
+ }
1217
+ setPrefillText(prefill) {
1218
+ this.setState({
1219
+ prefillText: prefill
1220
+ });
1221
+ }
1222
+ dismissOutboundMessage() {
1223
+ const outbound = this.state.outboundMessage;
1224
+ if (outbound?.id) this.track('widget_outbound_dismissed', {
1225
+ outboundId: outbound.id
1226
+ });
1227
+ this.setState({
1228
+ outboundMessage: null
1229
+ });
1230
+ }
1231
+
1232
+ // ─── Conversations & messages ───────────────────────────────────────────
1233
+
1234
+ async refreshConversations() {
1235
+ if (!this.state.booted) return;
1236
+ const epoch = this.epoch;
1237
+ try {
1238
+ const result = await this.client.listConversations();
1239
+ // Never repaint a signed-out (or switched) visitor's list.
1240
+ if (this.isStale(epoch)) return;
1241
+ this.setState(current => {
1242
+ // Reconcile unread with the server so replies that arrived during a
1243
+ // background gap still count (same rule as the boot-time seeding).
1244
+ const unreadByConversation = {};
1245
+ for (const conversation of result.data) {
1246
+ const count = typeof conversation.unreadCount === 'number' ? conversation.unreadCount : current.unreadByConversation[conversation.id] ?? 0;
1247
+ if (count > 0) unreadByConversation[conversation.id] = count;
1248
+ }
1249
+ // The thread on screen is being read right now — never re-badge it.
1250
+ if (current.activeConversationId && this.isThreadOnScreen(current.activeConversationId)) {
1251
+ delete unreadByConversation[current.activeConversationId];
1252
+ }
1253
+ this.persistUnread(unreadByConversation);
1254
+ return {
1255
+ conversations: result.data.map(conversation => this.hydrateConversation(conversation)),
1256
+ unreadByConversation
1257
+ };
1258
+ });
1259
+ } catch {
1260
+ // Keep whatever we already have — the list refreshes again on next open.
1261
+ }
1262
+ }
1263
+ async openConversation(conversationId) {
1264
+ this.threadGeneration += 1;
1265
+ if (!this.state.booted) {
1266
+ this.pendingConversationId = conversationId;
1267
+ this.setState({
1268
+ isOpen: true,
1269
+ activeTab: 'messages'
1270
+ });
1271
+ return;
1272
+ }
1273
+ const generation = this.threadGeneration;
1274
+ const unreadByConversation = this.clearUnreadFor(conversationId);
1275
+ this.setState({
1276
+ isOpen: true,
1277
+ activeTab: 'messages',
1278
+ activeConversationId: conversationId,
1279
+ messages: [],
1280
+ messagesLoading: true,
1281
+ hasMoreMessages: false,
1282
+ messageCursor: null,
1283
+ view: 'chat',
1284
+ unreadByConversation,
1285
+ agentTyping: false
1286
+ });
1287
+ // Loading the thread advances the server-side read cursor; hosts listen
1288
+ // to this to dismiss the conversation's push notification from the tray.
1289
+ this.events.emit(_events.RepzoWidgetEvents.ConversationDidRead, {
1290
+ conversationId
1291
+ });
1292
+ const epoch = this.epoch;
1293
+ try {
1294
+ const result = await this.client.listMessages(conversationId);
1295
+ // Opening A then B quickly: A's page must never land under B.
1296
+ if (this.isStale(epoch) || this.threadGeneration !== generation) return;
1297
+ this.setState(current => {
1298
+ // Live replies and optimistic sends may have arrived since this page
1299
+ // was requested. Keep them, preferring the live copy for shared ids.
1300
+ const liveIds = new Set(current.messages.map(message => message.id));
1301
+ const history = this.hydrateMessages(result.data).filter(message => !liveIds.has(message.id));
1302
+ return {
1303
+ messages: sortByCreatedAt([...history, ...current.messages]),
1304
+ messagesLoading: false,
1305
+ hasMoreMessages: result.meta.hasMore,
1306
+ messageCursor: result.meta.nextCursor
1307
+ };
1308
+ });
1309
+ } catch {
1310
+ if (!this.isStale(epoch) && this.threadGeneration === generation) {
1311
+ this.setState({
1312
+ messagesLoading: false
1313
+ });
1314
+ }
1315
+ }
1316
+ }
1317
+ async loadMoreMessages() {
1318
+ const {
1319
+ activeConversationId,
1320
+ hasMoreMessages,
1321
+ messagesLoading,
1322
+ messageCursor
1323
+ } = this.state;
1324
+ if (!activeConversationId || !hasMoreMessages || messagesLoading || !messageCursor) return;
1325
+ this.setState({
1326
+ messagesLoading: true
1327
+ });
1328
+ const epoch = this.epoch;
1329
+ const generation = this.threadGeneration;
1330
+ const isCurrent = () => !this.isStale(epoch) && this.threadGeneration === generation && this.state.activeConversationId === activeConversationId;
1331
+ try {
1332
+ const result = await this.client.listMessages(activeConversationId, messageCursor);
1333
+ // An older page for a thread that is no longer on screen goes nowhere.
1334
+ if (!isCurrent()) return;
1335
+ this.setState(current => {
1336
+ const known = new Set(current.messages.map(message => message.id));
1337
+ const older = this.hydrateMessages(result.data).filter(message => !known.has(message.id));
1338
+ return {
1339
+ messages: [...older, ...current.messages],
1340
+ messagesLoading: false,
1341
+ hasMoreMessages: result.meta.hasMore,
1342
+ messageCursor: result.meta.nextCursor
1343
+ };
1344
+ });
1345
+ } catch {
1346
+ if (isCurrent()) this.setState({
1347
+ messagesLoading: false
1348
+ });
1349
+ }
1350
+ }
1351
+
1352
+ /**
1353
+ * Re-reads the newest page of the thread on screen and merges in whatever
1354
+ * the stream missed (reconnect, foreground, re-init). Deduped by server id.
1355
+ * Only for a thread actually on screen: the first page also advances the
1356
+ * server read cursor, which must not happen for a thread nobody is reading.
1357
+ */
1358
+ async refreshOpenThread() {
1359
+ const conversationId = this.state.activeConversationId;
1360
+ if (!this.state.booted || !conversationId || !this.isThreadOnScreen(conversationId)) return;
1361
+ // A load already in flight owns the list (and its cursor) — let it land.
1362
+ if (this.state.messagesLoading) return;
1363
+ const epoch = this.epoch;
1364
+ const generation = this.threadGeneration;
1365
+ try {
1366
+ const result = await this.client.listMessages(conversationId);
1367
+ if (this.isStale(epoch) || this.threadGeneration !== generation) return;
1368
+ if (this.state.activeConversationId !== conversationId) return;
1369
+ const fresh = this.hydrateMessages(result.data);
1370
+ this.setState(current => {
1371
+ const known = new Set(current.messages.map(message => message.id));
1372
+ const missing = fresh.filter(message => !known.has(message.id));
1373
+ if (missing.length === 0) return {};
1374
+ const local = current.messages.filter(isLocalMessage);
1375
+ const hasServerMessages = local.length < current.messages.length;
1376
+ // No overlap means the gap is longer than a page: restart from the
1377
+ // newest page (keeping unsent bubbles) rather than leave a hole.
1378
+ if (missing.length === fresh.length || !hasServerMessages) {
1379
+ return {
1380
+ messages: [...fresh, ...local],
1381
+ hasMoreMessages: result.meta.hasMore,
1382
+ messageCursor: result.meta.nextCursor
1383
+ };
1384
+ }
1385
+ return {
1386
+ messages: sortByCreatedAt([...current.messages, ...missing])
1387
+ };
1388
+ });
1389
+ // The server just moved the read cursor — mirror it locally.
1390
+ if (this.state.unreadByConversation[conversationId]) {
1391
+ this.setState({
1392
+ unreadByConversation: this.clearUnreadFor(conversationId)
1393
+ });
1394
+ }
1395
+ } catch {
1396
+ // The next reconnect or foreground tries again.
1397
+ }
1398
+ }
1399
+
1400
+ /** A history page (newest-first) as the oldest-first list the UI renders. */
1401
+ hydrateMessages(page) {
1402
+ return page.map(message => ({
1403
+ ...message,
1404
+ attachments: this.hydrateAttachments(message.attachments, message.id)
1405
+ })).reverse();
1406
+ }
1407
+ async sendMessage(body, attachments = [], agentUiAction) {
1408
+ const trimmed = body.trim();
1409
+ if (!trimmed && attachments.length === 0) return;
1410
+ const {
1411
+ visitorProfile
1412
+ } = this.state;
1413
+ const generation = this.threadGeneration;
1414
+ const tempId = `temp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1415
+ const optimistic = {
1416
+ id: tempId,
1417
+ messageType: 'message',
1418
+ direction: 'inbound',
1419
+ senderType: 'visitor',
1420
+ senderName: visitorProfile.name || 'You',
1421
+ body: trimmed ? `<p>${trimmed}</p>` : '',
1422
+ bodyPlain: trimmed,
1423
+ attachments: attachments.length > 0 ? attachments : null,
1424
+ metadata: null,
1425
+ status: 'pending',
1426
+ createdAt: new Date().toISOString()
1427
+ };
1428
+ this.setState(current => ({
1429
+ messages: [...current.messages, optimistic],
1430
+ prefillText: ''
1431
+ }));
1432
+ this.track('widget_message_sent', {
1433
+ hasText: !!trimmed,
1434
+ attachmentCount: attachments.length
1435
+ });
1436
+ const markFailed = () => this.setState(current => ({
1437
+ messages: current.messages.map(message => message.id === tempId ? {
1438
+ ...message,
1439
+ status: 'failed'
1440
+ } : message)
1441
+ }));
1442
+ const epoch = this.epoch;
1443
+
1444
+ // A draft's first send creates the conversation. Anything sent from the
1445
+ // same draft before that POST answers waits for its id rather than
1446
+ // creating a second thread; the bubble is already on screen meanwhile.
1447
+ let conversationId = this.state.activeConversationId;
1448
+ let ownCreation = null;
1449
+ while (!conversationId) {
1450
+ const creation = this.conversationCreation;
1451
+ if (!creation || creation.generation !== generation) {
1452
+ ownCreation = createConversationCreation(generation);
1453
+ this.conversationCreation = ownCreation;
1454
+ break;
1455
+ }
1456
+ conversationId = await creation.promise;
1457
+ if (this.isStale(epoch)) return;
1458
+ // The creating send failed and the visitor has since left the draft:
1459
+ // don't open a thread nobody is looking at.
1460
+ if (!conversationId && this.threadGeneration !== generation) {
1461
+ markFailed();
1462
+ return;
1463
+ }
1464
+ }
1465
+ let createdId = null;
1466
+ try {
1467
+ const result = await this.client.sendMessage({
1468
+ conversationId: conversationId || undefined,
1469
+ forceNewConversation: !conversationId,
1470
+ body: trimmed,
1471
+ attachments: attachments.length > 0 ? attachments : undefined,
1472
+ visitorName: visitorProfile.name,
1473
+ visitorEmail: visitorProfile.email,
1474
+ agentUiAction
1475
+ });
1476
+ if (this.isStale(epoch)) return;
1477
+ if (ownCreation) createdId = result.conversationId;
1478
+ this.setState(current => {
1479
+ // The stream or a catch-up fetch may have delivered this message
1480
+ // already — keep one copy, never two bubbles with the same id.
1481
+ const delivered = current.messages.some(message => message.id === result.messageId);
1482
+ const messages = delivered ? current.messages.filter(message => message.id !== tempId) : current.messages.map(message => message.id === tempId ? {
1483
+ ...message,
1484
+ id: result.messageId,
1485
+ status: 'sent'
1486
+ } : message);
1487
+ if (!createdId) return {
1488
+ messages
1489
+ };
1490
+ const known = current.conversations.some(item => item.id === result.conversationId);
1491
+ const conversation = {
1492
+ id: result.conversationId,
1493
+ subject: null,
1494
+ status: 'open',
1495
+ snippet: trimmed ? trimmed.slice(0, 100) : `${attachments.length} attachment${attachments.length === 1 ? '' : 's'}`,
1496
+ messageCount: 1,
1497
+ lastMessageAt: new Date().toISOString(),
1498
+ createdAt: new Date().toISOString(),
1499
+ agentName: null,
1500
+ agentAvatar: null
1501
+ };
1502
+ return {
1503
+ messages,
1504
+ conversations: known ? current.conversations : [this.applyConversationAgent(conversation), ...current.conversations],
1505
+ // Only adopt the thread if the visitor is still on this draft.
1506
+ ...(this.threadGeneration === generation ? {
1507
+ activeConversationId: result.conversationId
1508
+ } : {})
1509
+ };
1510
+ });
1511
+ } catch (error) {
1512
+ if (this.isStale(epoch)) throw error;
1513
+ markFailed();
1514
+ throw error;
1515
+ } finally {
1516
+ if (ownCreation) {
1517
+ if (this.conversationCreation === ownCreation) this.conversationCreation = null;
1518
+ // Resolves null on failure: the next queued send then creates it.
1519
+ ownCreation.resolve(createdId);
1520
+ // Anything held back for the draft now has a home (or goes to unread).
1521
+ this.replayDraftEvents();
1522
+ }
1523
+ }
1524
+ }
1525
+ async identifyVisitor(data) {
1526
+ this.setState(current => ({
1527
+ visitorProfile: {
1528
+ ...current.visitorProfile,
1529
+ ...(data.name ? {
1530
+ name: data.name
1531
+ } : {}),
1532
+ ...(data.email ? {
1533
+ email: data.email
1534
+ } : {}),
1535
+ ...(data.phone ? {
1536
+ phone: data.phone
1537
+ } : {})
1538
+ }
1539
+ }));
1540
+ await this.client.identify(data);
1541
+ }
1542
+
1543
+ /** Fire-and-forget visitor typing ping, throttled well under the 30/min cap. */
1544
+ notifyTyping(isTyping) {
1545
+ const conversationId = this.state.activeConversationId;
1546
+ if (!conversationId) return;
1547
+ const now = Date.now();
1548
+ if (isTyping) {
1549
+ if (now - this.lastTypingPingAt < TYPING_PING_INTERVAL_MS) return;
1550
+ this.lastTypingPingAt = now;
1551
+ } else {
1552
+ // Nothing to stop — skip the round trip rather than burn rate limit.
1553
+ if (!this.visitorTypingActive) return;
1554
+ this.lastTypingPingAt = 0;
1555
+ }
1556
+ this.visitorTypingActive = isTyping;
1557
+ this.client.sendTyping(conversationId, isTyping).catch(() => {});
1558
+ }
1559
+
1560
+ // ─── Analytics & outbound ───────────────────────────────────────────────
1561
+
1562
+ track(event, metadata = {}) {
1563
+ if (!this.state.booted || this.state.config?.analyticsEnabled === false) return;
1564
+ this.client.trackEvent({
1565
+ event,
1566
+ visitorId: this.state.identityId,
1567
+ metadata: {
1568
+ locale: this.state.locale,
1569
+ platform: _reactNative.Platform.OS,
1570
+ ...metadata
1571
+ }
1572
+ }).catch(() => {});
1573
+ }
1574
+ loadOutboundMessage() {
1575
+ if (this.state.outboundMessage) return;
1576
+ const epoch = this.epoch;
1577
+ this.client.getOutboundMessage().then(({
1578
+ message
1579
+ }) => {
1580
+ if (!message || this.isStale(epoch)) return;
1581
+ this.setState({
1582
+ outboundMessage: {
1583
+ ...message,
1584
+ imageUrl: this.client.resolveUrl(message.imageUrl) ?? undefined
1585
+ }
1586
+ });
1587
+ }).catch(() => {});
1588
+ }
1589
+
1590
+ // ─── Teardown (tests / hot reload) ──────────────────────────────────────
1591
+
1592
+ destroy() {
1593
+ this.realtime.close();
1594
+ this.clearTypingTimer();
1595
+ this.resetThreadTracking();
1596
+ this.appStateSubscription?.remove();
1597
+ this.appStateSubscription = null;
1598
+ this.subscribers.clear();
1599
+ this.events.removeAllListeners();
1600
+ }
1601
+ }
1602
+ exports.WidgetStore = WidgetStore;
1603
+ function createConversationCreation(generation) {
1604
+ let resolve = () => {};
1605
+ const promise = new Promise(settle => {
1606
+ resolve = settle;
1607
+ });
1608
+ return {
1609
+ generation,
1610
+ promise,
1611
+ resolve
1612
+ };
1613
+ }
1614
+
1615
+ /** Optimistic visitor bubbles the server hasn't acknowledged (pending/failed). */
1616
+ function isLocalMessage(message) {
1617
+ return message.id.startsWith('temp-');
1618
+ }
1619
+
1620
+ /** Stable, so same-instant rows keep their order; bad dates sort as oldest. */
1621
+ function sortByCreatedAt(messages) {
1622
+ const time = message => Date.parse(message.createdAt) || 0;
1623
+ return [...messages].sort((a, b) => time(a) - time(b));
1624
+ }
1625
+ function identityKey(user) {
1626
+ return (user.email || user.userId || '').toLowerCase().trim();
1627
+ }
1628
+
1629
+ /**
1630
+ * The visitor token travels on every request, so plain http is refused except
1631
+ * for local development: localhost, the Android emulator's host alias,
1632
+ * `*.local` hosts, or any host in a `__DEV__` build.
1633
+ */
1634
+ function assertSecureBaseUrl(baseUrl) {
1635
+ const match = /^(https?):\/\/(\[[^\]]+\]|[^/?#:]+)(?::\d+)?(?:[/?#]|$)/i.exec(baseUrl);
1636
+ if (!match) {
1637
+ throw new _client.RepzoWidgetError(`boot(): baseUrl must be an absolute https:// URL (got "${baseUrl}")`, 0, 'http');
1638
+ }
1639
+ const scheme = match[1].toLowerCase();
1640
+ const host = match[2].toLowerCase();
1641
+ if (scheme === 'https') return baseUrl;
1642
+ const isDevBuild = typeof __DEV__ !== 'undefined' && __DEV__;
1643
+ if (isDevBuild || LOCAL_DEV_HOSTS.has(host) || host.endsWith('.local')) return baseUrl;
1644
+ throw new _client.RepzoWidgetError(`boot(): baseUrl must use https:// (got "${baseUrl}"). Plain http is only allowed for ` + 'localhost, 127.0.0.1, 10.0.2.2, *.local hosts, or in __DEV__ builds.', 0, 'http');
1645
+ }
1646
+
1647
+ /** Waits for `promise` at most `ms`; never rejects. */
1648
+ function settleWithin(promise, ms) {
1649
+ return new Promise(resolve => {
1650
+ const timer = setTimeout(resolve, ms);
1651
+ const done = () => {
1652
+ clearTimeout(timer);
1653
+ resolve();
1654
+ };
1655
+ promise.then(done, done);
1656
+ });
1657
+ }
1658
+ function firstEnabledSpace(spaces) {
1659
+ return spaces?.find(space => space.enabled)?.type ?? null;
1660
+ }
1661
+
1662
+ /** The server rejects oversized bags; drop junk here so a typo can't 400 a login. */
1663
+ function sanitizeAttributes(attributes) {
1664
+ const result = {};
1665
+ if (!attributes || typeof attributes !== 'object') return result;
1666
+ for (const [key, value] of Object.entries(attributes)) {
1667
+ if (!key || key.length > 64) continue;
1668
+ if (value === null || value === undefined) continue;
1669
+ if (typeof value === 'string') {
1670
+ if (!value.trim()) continue;
1671
+ result[key] = value.slice(0, 512);
1672
+ } else if (typeof value === 'number') {
1673
+ if (!Number.isFinite(value)) continue;
1674
+ result[key] = value;
1675
+ } else if (typeof value === 'boolean') {
1676
+ result[key] = value;
1677
+ }
1678
+ if (Object.keys(result).length >= 30) break;
1679
+ }
1680
+ return result;
1681
+ }
1682
+ //# sourceMappingURL=widget-store.js.map