@anker-in/campaign-ui 0.3.3 → 0.3.5

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 (224) hide show
  1. package/dist/cjs/components/LiveChatWidget/LiveChatWidget.d.ts +21 -1
  2. package/dist/cjs/components/LiveChatWidget/LiveChatWidget.js +1 -1
  3. package/dist/cjs/components/LiveChatWidget/LiveChatWidget.js.map +3 -3
  4. package/dist/cjs/components/LiveChatWidget/api/chat.d.ts +23 -2
  5. package/dist/cjs/components/LiveChatWidget/api/chat.js +2 -2
  6. package/dist/cjs/components/LiveChatWidget/api/chat.js.map +3 -3
  7. package/dist/cjs/components/LiveChatWidget/components/ChatHeader.js +1 -1
  8. package/dist/cjs/components/LiveChatWidget/components/ChatHeader.js.map +2 -2
  9. package/dist/cjs/components/LiveChatWidget/components/ChatInput.d.ts +5 -0
  10. package/dist/cjs/components/LiveChatWidget/components/ChatInput.js +1 -1
  11. package/dist/cjs/components/LiveChatWidget/components/ChatInput.js.map +3 -3
  12. package/dist/cjs/components/LiveChatWidget/components/ChatMessage.js +2 -2
  13. package/dist/cjs/components/LiveChatWidget/components/ChatMessage.js.map +3 -3
  14. package/dist/cjs/components/LiveChatWidget/components/ChatWindow.d.ts +5 -0
  15. package/dist/cjs/components/LiveChatWidget/components/ChatWindow.js +1 -1
  16. package/dist/cjs/components/LiveChatWidget/components/ChatWindow.js.map +3 -3
  17. package/dist/cjs/components/LiveChatWidget/components/ComplianceDialog.d.ts +51 -0
  18. package/dist/cjs/components/LiveChatWidget/components/ComplianceDialog.js +33 -0
  19. package/dist/cjs/components/LiveChatWidget/components/ComplianceDialog.js.map +7 -0
  20. package/dist/cjs/components/LiveChatWidget/components/MessageContent/CartCard.js +1 -1
  21. package/dist/cjs/components/LiveChatWidget/components/MessageContent/CartCard.js.map +3 -3
  22. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ErrorBlock.js +1 -1
  23. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ErrorBlock.js.map +2 -2
  24. package/dist/cjs/components/LiveChatWidget/components/MessageContent/FAQList.js +1 -1
  25. package/dist/cjs/components/LiveChatWidget/components/MessageContent/FAQList.js.map +3 -3
  26. package/dist/cjs/components/LiveChatWidget/components/MessageContent/PolicyBlock.js +2 -2
  27. package/dist/cjs/components/LiveChatWidget/components/MessageContent/PolicyBlock.js.map +3 -3
  28. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductCard.d.ts +17 -24
  29. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductCard.js +1 -4
  30. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductCard.js.map +3 -3
  31. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductComparison.d.ts +7 -1
  32. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductComparison.js +1 -1
  33. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductComparison.js.map +3 -3
  34. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductList.js +1 -1
  35. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductList.js.map +3 -3
  36. package/dist/cjs/components/LiveChatWidget/components/MessageContent/PromotionList.d.ts +4 -1
  37. package/dist/cjs/components/LiveChatWidget/components/MessageContent/PromotionList.js +1 -1
  38. package/dist/cjs/components/LiveChatWidget/components/MessageContent/PromotionList.js.map +3 -3
  39. package/dist/cjs/components/LiveChatWidget/components/MessageContent/QuickReplies.js +1 -1
  40. package/dist/cjs/components/LiveChatWidget/components/MessageContent/QuickReplies.js.map +2 -2
  41. package/dist/cjs/components/LiveChatWidget/components/MessageContent/TextBlock.js +1 -1
  42. package/dist/cjs/components/LiveChatWidget/components/MessageContent/TextBlock.js.map +3 -3
  43. package/dist/cjs/components/LiveChatWidget/components/MessageContent.js +1 -1
  44. package/dist/cjs/components/LiveChatWidget/components/MessageContent.js.map +2 -2
  45. package/dist/cjs/components/LiveChatWidget/components/MessageList.js +3 -3
  46. package/dist/cjs/components/LiveChatWidget/components/MessageList.js.map +3 -3
  47. package/dist/cjs/components/LiveChatWidget/constants.d.ts +5 -0
  48. package/dist/cjs/components/LiveChatWidget/constants.js +1 -1
  49. package/dist/cjs/components/LiveChatWidget/constants.js.map +3 -3
  50. package/dist/cjs/components/LiveChatWidget/hooks/useChatAPI.d.ts +9 -0
  51. package/dist/cjs/components/LiveChatWidget/hooks/useChatAPI.js +1 -1
  52. package/dist/cjs/components/LiveChatWidget/hooks/useChatAPI.js.map +3 -3
  53. package/dist/cjs/components/LiveChatWidget/hooks/useChatState.d.ts +36 -2
  54. package/dist/cjs/components/LiveChatWidget/hooks/useChatState.js +1 -1
  55. package/dist/cjs/components/LiveChatWidget/hooks/useChatState.js.map +3 -3
  56. package/dist/cjs/components/LiveChatWidget/index.d.ts +1 -1
  57. package/dist/cjs/components/LiveChatWidget/index.js +1 -1
  58. package/dist/cjs/components/LiveChatWidget/index.js.map +2 -2
  59. package/dist/cjs/components/LiveChatWidget/types.d.ts +213 -3
  60. package/dist/cjs/components/LiveChatWidget/types.js +1 -1
  61. package/dist/cjs/components/LiveChatWidget/types.js.map +1 -1
  62. package/dist/cjs/components/LiveChatWidget/utils/fetcher.d.ts +42 -0
  63. package/dist/cjs/components/LiveChatWidget/utils/fetcher.js +2 -0
  64. package/dist/cjs/components/LiveChatWidget/utils/fetcher.js.map +7 -0
  65. package/dist/cjs/components/chat/markdown.js +1 -1
  66. package/dist/cjs/components/chat/markdown.js.map +2 -2
  67. package/dist/cjs/components/index.d.ts +2 -0
  68. package/dist/cjs/components/index.js +1 -1
  69. package/dist/cjs/components/index.js.map +3 -3
  70. package/dist/cjs/stories/LiveChatWidget.stories.d.ts +1 -79
  71. package/dist/cjs/stories/LiveChatWidget.stories.js +3 -49
  72. package/dist/cjs/stories/LiveChatWidget.stories.js.map +3 -3
  73. package/dist/esm/components/LiveChatWidget/LiveChatWidget.d.ts +21 -1
  74. package/dist/esm/components/LiveChatWidget/LiveChatWidget.js +1 -1
  75. package/dist/esm/components/LiveChatWidget/LiveChatWidget.js.map +3 -3
  76. package/dist/esm/components/LiveChatWidget/api/chat.d.ts +23 -2
  77. package/dist/esm/components/LiveChatWidget/api/chat.js +2 -2
  78. package/dist/esm/components/LiveChatWidget/api/chat.js.map +3 -3
  79. package/dist/esm/components/LiveChatWidget/components/ChatHeader.js +1 -1
  80. package/dist/esm/components/LiveChatWidget/components/ChatHeader.js.map +2 -2
  81. package/dist/esm/components/LiveChatWidget/components/ChatInput.d.ts +5 -0
  82. package/dist/esm/components/LiveChatWidget/components/ChatInput.js +1 -1
  83. package/dist/esm/components/LiveChatWidget/components/ChatInput.js.map +3 -3
  84. package/dist/esm/components/LiveChatWidget/components/ChatMessage.js +2 -2
  85. package/dist/esm/components/LiveChatWidget/components/ChatMessage.js.map +3 -3
  86. package/dist/esm/components/LiveChatWidget/components/ChatWindow.d.ts +5 -0
  87. package/dist/esm/components/LiveChatWidget/components/ChatWindow.js +1 -1
  88. package/dist/esm/components/LiveChatWidget/components/ChatWindow.js.map +3 -3
  89. package/dist/esm/components/LiveChatWidget/components/ComplianceDialog.d.ts +51 -0
  90. package/dist/esm/components/LiveChatWidget/components/ComplianceDialog.js +33 -0
  91. package/dist/esm/components/LiveChatWidget/components/ComplianceDialog.js.map +7 -0
  92. package/dist/esm/components/LiveChatWidget/components/MessageContent/CartCard.js +1 -1
  93. package/dist/esm/components/LiveChatWidget/components/MessageContent/CartCard.js.map +3 -3
  94. package/dist/esm/components/LiveChatWidget/components/MessageContent/ErrorBlock.js +1 -1
  95. package/dist/esm/components/LiveChatWidget/components/MessageContent/ErrorBlock.js.map +2 -2
  96. package/dist/esm/components/LiveChatWidget/components/MessageContent/FAQList.js +1 -1
  97. package/dist/esm/components/LiveChatWidget/components/MessageContent/FAQList.js.map +3 -3
  98. package/dist/esm/components/LiveChatWidget/components/MessageContent/PolicyBlock.js +2 -2
  99. package/dist/esm/components/LiveChatWidget/components/MessageContent/PolicyBlock.js.map +3 -3
  100. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductCard.d.ts +17 -24
  101. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductCard.js +1 -4
  102. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductCard.js.map +3 -3
  103. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductComparison.d.ts +7 -1
  104. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductComparison.js +1 -1
  105. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductComparison.js.map +3 -3
  106. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductList.js +1 -1
  107. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductList.js.map +3 -3
  108. package/dist/esm/components/LiveChatWidget/components/MessageContent/PromotionList.d.ts +4 -1
  109. package/dist/esm/components/LiveChatWidget/components/MessageContent/PromotionList.js +1 -1
  110. package/dist/esm/components/LiveChatWidget/components/MessageContent/PromotionList.js.map +3 -3
  111. package/dist/esm/components/LiveChatWidget/components/MessageContent/QuickReplies.js +1 -1
  112. package/dist/esm/components/LiveChatWidget/components/MessageContent/QuickReplies.js.map +2 -2
  113. package/dist/esm/components/LiveChatWidget/components/MessageContent/TextBlock.js +1 -1
  114. package/dist/esm/components/LiveChatWidget/components/MessageContent/TextBlock.js.map +3 -3
  115. package/dist/esm/components/LiveChatWidget/components/MessageContent.js +1 -1
  116. package/dist/esm/components/LiveChatWidget/components/MessageContent.js.map +2 -2
  117. package/dist/esm/components/LiveChatWidget/components/MessageList.js +3 -3
  118. package/dist/esm/components/LiveChatWidget/components/MessageList.js.map +3 -3
  119. package/dist/esm/components/LiveChatWidget/constants.d.ts +5 -0
  120. package/dist/esm/components/LiveChatWidget/constants.js +1 -1
  121. package/dist/esm/components/LiveChatWidget/constants.js.map +3 -3
  122. package/dist/esm/components/LiveChatWidget/hooks/useChatAPI.d.ts +9 -0
  123. package/dist/esm/components/LiveChatWidget/hooks/useChatAPI.js +1 -1
  124. package/dist/esm/components/LiveChatWidget/hooks/useChatAPI.js.map +3 -3
  125. package/dist/esm/components/LiveChatWidget/hooks/useChatState.d.ts +36 -2
  126. package/dist/esm/components/LiveChatWidget/hooks/useChatState.js +1 -1
  127. package/dist/esm/components/LiveChatWidget/hooks/useChatState.js.map +3 -3
  128. package/dist/esm/components/LiveChatWidget/index.d.ts +1 -1
  129. package/dist/esm/components/LiveChatWidget/index.js +1 -1
  130. package/dist/esm/components/LiveChatWidget/index.js.map +2 -2
  131. package/dist/esm/components/LiveChatWidget/types.d.ts +213 -3
  132. package/dist/esm/components/LiveChatWidget/utils/fetcher.d.ts +42 -0
  133. package/dist/esm/components/LiveChatWidget/utils/fetcher.js +2 -0
  134. package/dist/esm/components/LiveChatWidget/utils/fetcher.js.map +7 -0
  135. package/dist/esm/components/chat/markdown.js +1 -1
  136. package/dist/esm/components/chat/markdown.js.map +2 -2
  137. package/dist/esm/components/index.d.ts +2 -0
  138. package/dist/esm/components/index.js +1 -1
  139. package/dist/esm/components/index.js.map +3 -3
  140. package/dist/esm/stories/LiveChatWidget.stories.d.ts +1 -79
  141. package/dist/esm/stories/LiveChatWidget.stories.js +3 -49
  142. package/dist/esm/stories/LiveChatWidget.stories.js.map +3 -3
  143. package/dist/index.d.mts +1305 -0
  144. package/dist/index.d.ts +1305 -0
  145. package/dist/index.js +26656 -0
  146. package/dist/index.js.map +1 -0
  147. package/dist/index.mjs +26641 -0
  148. package/dist/index.mjs.map +1 -0
  149. package/package.json +8 -1
  150. package/src/components/LiveChatWidget/LiveChatWidget.tsx +907 -0
  151. package/src/components/LiveChatWidget/api/chat.ts +175 -0
  152. package/src/components/LiveChatWidget/components/ChatBubble.tsx +152 -0
  153. package/src/components/LiveChatWidget/components/ChatHeader.tsx +150 -0
  154. package/src/components/LiveChatWidget/components/ChatInput.tsx +253 -0
  155. package/src/components/LiveChatWidget/components/ChatMessage.tsx +190 -0
  156. package/src/components/LiveChatWidget/components/ChatWindow.tsx +363 -0
  157. package/src/components/LiveChatWidget/components/ComplianceDialog.tsx +216 -0
  158. package/src/components/LiveChatWidget/components/MessageContent/CartCard.tsx +202 -0
  159. package/src/components/LiveChatWidget/components/MessageContent/ErrorBlock.tsx +75 -0
  160. package/src/components/LiveChatWidget/components/MessageContent/FAQList.tsx +128 -0
  161. package/src/components/LiveChatWidget/components/MessageContent/PolicyBlock.tsx +152 -0
  162. package/src/components/LiveChatWidget/components/MessageContent/ProductCard.tsx +227 -0
  163. package/src/components/LiveChatWidget/components/MessageContent/ProductComparison.tsx +377 -0
  164. package/src/components/LiveChatWidget/components/MessageContent/ProductList.tsx +293 -0
  165. package/src/components/LiveChatWidget/components/MessageContent/PromotionList.tsx +170 -0
  166. package/src/components/LiveChatWidget/components/MessageContent/QuickReplies.tsx +91 -0
  167. package/src/components/LiveChatWidget/components/MessageContent/TextBlock.tsx +110 -0
  168. package/src/components/LiveChatWidget/components/MessageContent/ThinkingBlock.tsx +53 -0
  169. package/src/components/LiveChatWidget/components/MessageContent/index.ts +16 -0
  170. package/src/components/LiveChatWidget/components/MessageContent.tsx +113 -0
  171. package/src/components/LiveChatWidget/components/MessageList.tsx +256 -0
  172. package/src/components/LiveChatWidget/components/ScrollAnchor.tsx +75 -0
  173. package/src/components/LiveChatWidget/constants.ts +36 -0
  174. package/src/components/LiveChatWidget/hooks/useChatAPI.ts +146 -0
  175. package/src/components/LiveChatWidget/hooks/useChatState.ts +1091 -0
  176. package/src/components/LiveChatWidget/hooks/useSession.ts +123 -0
  177. package/src/components/LiveChatWidget/index.tsx +63 -0
  178. package/src/components/LiveChatWidget/types.ts +1012 -0
  179. package/src/components/LiveChatWidget/utils/cartTransformers.ts +72 -0
  180. package/src/components/LiveChatWidget/utils/fetcher.ts +131 -0
  181. package/src/components/LiveChatWidget/utils/messageRenderers.ts +120 -0
  182. package/src/components/LiveChatWidget/utils/productTransformers.ts +149 -0
  183. package/src/components/LiveChatWidget/utils/userId.ts +140 -0
  184. package/src/components/LiveChatWidget/utils/validation.ts +99 -0
  185. package/src/components/chat/markdown.tsx +1 -1
  186. package/src/components/index.ts +23 -0
  187. package/src/stories/LiveChatWidget.stories.tsx +317 -0
  188. package/src/styles/livechat.css +346 -0
  189. package/dist/cjs/components/credits/context/hooks/useFunctionMemberPrice.d.ts +0 -7
  190. package/dist/cjs/components/credits/context/hooks/useFunctionMemberPrice.js +0 -2
  191. package/dist/cjs/components/credits/context/hooks/useFunctionMemberPrice.js.map +0 -7
  192. package/dist/cjs/components/credits/context/utils/atobID.d.ts +0 -1
  193. package/dist/cjs/components/credits/context/utils/atobID.js +0 -2
  194. package/dist/cjs/components/credits/context/utils/atobID.js.map +0 -7
  195. package/dist/cjs/components/credits/context/utils/functionDiscountCalculate.d.ts +0 -5
  196. package/dist/cjs/components/credits/context/utils/functionDiscountCalculate.js +0 -2
  197. package/dist/cjs/components/credits/context/utils/functionDiscountCalculate.js.map +0 -7
  198. package/dist/cjs/components/credits/context/utils/getFunctionMemberPrice.d.ts +0 -8
  199. package/dist/cjs/components/credits/context/utils/getFunctionMemberPrice.js +0 -2
  200. package/dist/cjs/components/credits/context/utils/getFunctionMemberPrice.js.map +0 -7
  201. package/dist/cjs/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.d.ts +0 -9
  202. package/dist/cjs/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.js +0 -2
  203. package/dist/cjs/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.js.map +0 -7
  204. package/dist/cjs/components/credits/context/utils/variantGetCoupon.d.ts +0 -6
  205. package/dist/cjs/components/credits/context/utils/variantGetCoupon.js +0 -2
  206. package/dist/cjs/components/credits/context/utils/variantGetCoupon.js.map +0 -7
  207. package/dist/esm/components/credits/context/hooks/useFunctionMemberPrice.d.ts +0 -7
  208. package/dist/esm/components/credits/context/hooks/useFunctionMemberPrice.js +0 -2
  209. package/dist/esm/components/credits/context/hooks/useFunctionMemberPrice.js.map +0 -7
  210. package/dist/esm/components/credits/context/utils/atobID.d.ts +0 -1
  211. package/dist/esm/components/credits/context/utils/atobID.js +0 -2
  212. package/dist/esm/components/credits/context/utils/atobID.js.map +0 -7
  213. package/dist/esm/components/credits/context/utils/functionDiscountCalculate.d.ts +0 -5
  214. package/dist/esm/components/credits/context/utils/functionDiscountCalculate.js +0 -2
  215. package/dist/esm/components/credits/context/utils/functionDiscountCalculate.js.map +0 -7
  216. package/dist/esm/components/credits/context/utils/getFunctionMemberPrice.d.ts +0 -8
  217. package/dist/esm/components/credits/context/utils/getFunctionMemberPrice.js +0 -2
  218. package/dist/esm/components/credits/context/utils/getFunctionMemberPrice.js.map +0 -7
  219. package/dist/esm/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.d.ts +0 -9
  220. package/dist/esm/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.js +0 -2
  221. package/dist/esm/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.js.map +0 -7
  222. package/dist/esm/components/credits/context/utils/variantGetCoupon.d.ts +0 -6
  223. package/dist/esm/components/credits/context/utils/variantGetCoupon.js +0 -2
  224. package/dist/esm/components/credits/context/utils/variantGetCoupon.js.map +0 -7
@@ -0,0 +1,72 @@
1
+ /**
2
+ * 购物车数据转换工具
3
+ * 将后端返回的 Shopify GraphQL 格式转换为前端标准格式
4
+ */
5
+
6
+ import type { BackendCartData, CartData, CartLine } from '../types'
7
+
8
+ /**
9
+ * 转换后端购物车数据为前端格式
10
+ *
11
+ * @param backendData 后端返回的 Shopify GraphQL 格式数据
12
+ * @returns 前端标准化的购物车数据
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * const backendData = {
17
+ * id: "gid://shopify/Cart/xxx",
18
+ * lines: { edges: [{ node: {...} }] },
19
+ * cost: { totalAmount: {...}, subtotalAmount: {...} },
20
+ * ...
21
+ * }
22
+ *
23
+ * const cartData = transformCartData(backendData)
24
+ * // => { cartId: "gid://shopify/Cart/xxx", lines: [...], ... }
25
+ * ```
26
+ */
27
+ export function transformCartData(backendData: BackendCartData): CartData {
28
+ // 转换商品行数据:从 edges[].node 结构转为扁平数组
29
+ const lines: CartLine[] = backendData.lines.edges.map(edge => ({
30
+ id: edge.node.id,
31
+ quantity: edge.node.quantity,
32
+ cost: {
33
+ totalAmount: edge.node.cost.totalAmount,
34
+ amountPerQuantity: edge.node.cost.amountPerQuantity,
35
+ subtotalAmount: edge.node.cost.subtotalAmount,
36
+ },
37
+ merchandise: {
38
+ id: edge.node.merchandise.id,
39
+ title: edge.node.merchandise.title,
40
+ price: edge.node.merchandise.price,
41
+ image: edge.node.merchandise.image
42
+ ? {
43
+ url: edge.node.merchandise.image.url,
44
+ altText: edge.node.merchandise.image.altText || undefined,
45
+ }
46
+ : undefined,
47
+ product: {
48
+ id: edge.node.merchandise.product.id,
49
+ title: edge.node.merchandise.product.title,
50
+ handle: edge.node.merchandise.product.handle,
51
+ },
52
+ },
53
+ attributes: edge.node.attributes,
54
+ }))
55
+
56
+ // 返回标准化的购物车数据
57
+ return {
58
+ isEmpty: backendData.totalQuantity === 0 || lines.length === 0,
59
+ cartId: backendData.id,
60
+ totalQuantity: backendData.totalQuantity,
61
+ lines,
62
+ cost: {
63
+ totalAmount: backendData.cost.totalAmount,
64
+ subtotalAmount: backendData.cost.subtotalAmount,
65
+ },
66
+ discountCodes: backendData.discountCodes?.map((code: any) => ({
67
+ code: code.code || code,
68
+ applicable: code.applicable !== false,
69
+ })),
70
+ checkoutUrl: backendData.checkoutUrl,
71
+ }
72
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * LiveChat Fetcher
3
+ * 参照 storefront 的实现,支持 reCAPTCHA
4
+ */
5
+
6
+ /**
7
+ * 执行 Google reCAPTCHA 验证
8
+ */
9
+ const executeRecaptcha = async (action: string, sitekey: string): Promise<string | false> => {
10
+ if (typeof window === 'undefined' || !window.grecaptcha?.enterprise?.execute) {
11
+ console.warn('[LiveChat Fetcher] reCAPTCHA not loaded')
12
+ return false
13
+ }
14
+
15
+ try {
16
+ const token = await window.grecaptcha.enterprise.execute(sitekey, { action })
17
+ return token
18
+ } catch (error) {
19
+ console.error('[LiveChat Fetcher] reCAPTCHA execution failed:', error)
20
+ return false
21
+ }
22
+ }
23
+
24
+ /**
25
+ * 获取 reCAPTCHA headers
26
+ */
27
+ async function getRecaptchaHeaders(
28
+ action: string,
29
+ sitekey: string,
30
+ headerKey = 'X-Recaptcha-Token'
31
+ ): Promise<Record<string, string>> {
32
+ const recaptchaToken = await executeRecaptcha(action, sitekey)
33
+ if (!recaptchaToken) {
34
+ return {}
35
+ }
36
+ return {
37
+ [headerKey]: recaptchaToken,
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Fetcher 参数
43
+ */
44
+ export interface FetcherOptions {
45
+ url: string
46
+ method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
47
+ headers?: Record<string, string>
48
+ body?: any
49
+ timeout?: number
50
+ needRecaptcha?: boolean
51
+ recaptchaSitekey?: string
52
+ recaptchaAction?: string
53
+ recaptchaHeaderKey?: string
54
+ }
55
+
56
+ /**
57
+ * Fetcher 函数
58
+ * 支持 reCAPTCHA 和自定义 headers
59
+ */
60
+ export const fetcher = async ({
61
+ url,
62
+ method = 'POST',
63
+ headers = {},
64
+ body,
65
+ timeout = 90000,
66
+ needRecaptcha = false,
67
+ recaptchaSitekey,
68
+ recaptchaAction = '',
69
+ recaptchaHeaderKey = 'X-Recaptcha-Token',
70
+ }: FetcherOptions): Promise<Response> => {
71
+ // 获取 reCAPTCHA headers(如果需要)
72
+ let recaptchaHeaders: Record<string, string> = {}
73
+ if (needRecaptcha) {
74
+ if (!recaptchaSitekey) {
75
+ console.warn('[LiveChat Fetcher] needRecaptcha=true but recaptchaSitekey is missing')
76
+ } else {
77
+ recaptchaHeaders = await getRecaptchaHeaders(recaptchaAction, recaptchaSitekey, recaptchaHeaderKey)
78
+ }
79
+ }
80
+
81
+ // 准备请求体
82
+ const bodyData = body ? JSON.stringify(body) : undefined
83
+
84
+ const controller = new AbortController()
85
+ let timeoutTimer: NodeJS.Timeout | undefined
86
+ if (timeout) {
87
+ timeoutTimer = setTimeout(() => controller.abort(), timeout)
88
+ }
89
+
90
+ try {
91
+ const response = await fetch(url, {
92
+ method,
93
+ mode: 'cors',
94
+ headers: {
95
+ 'Content-Type': 'application/json',
96
+ ...headers,
97
+ ...recaptchaHeaders,
98
+ },
99
+ signal: controller.signal,
100
+ ...(method !== 'GET' && bodyData && { body: bodyData }),
101
+ })
102
+
103
+ if (timeoutTimer) {
104
+ clearTimeout(timeoutTimer)
105
+ }
106
+
107
+ return response
108
+ } catch (error) {
109
+ if (timeoutTimer) {
110
+ clearTimeout(timeoutTimer)
111
+ }
112
+ throw error
113
+ }
114
+ }
115
+
116
+ /**
117
+ * 扩展 Window 接口以支持 grecaptcha
118
+ */
119
+ declare global {
120
+ interface Window {
121
+ grecaptcha?: {
122
+ execute: (sitekey: string, options: { action: string }) => Promise<string>
123
+ ready: (callback: () => void) => void
124
+ enterprise?: {
125
+ execute: (sitekey: string, options: { action: string }) =>
126
+ Promise<string>
127
+ ready: (callback: () => void) => void
128
+ }
129
+ }
130
+ }
131
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * 消息渲染器注册表
3
+ * 支持自定义消息类型的扩展机制
4
+ * 基于 specs/livechat-widget/plan.md 的扩展机制设计
5
+ */
6
+
7
+ import type { MessageContent, MessageRenderer } from '../types'
8
+
9
+ /**
10
+ * 消息渲染器注册表
11
+ * 允许注册自定义的消息类型渲染器
12
+ */
13
+ export class MessageRendererRegistry {
14
+ private renderers = new Map<string, MessageRenderer>()
15
+
16
+ /**
17
+ * 注册自定义渲染器
18
+ * @param type 消息内容类型
19
+ * @param renderer 渲染器实现
20
+ */
21
+ register(type: string, renderer: MessageRenderer): void {
22
+ if (!type || typeof type !== 'string') {
23
+ throw new Error('Message type must be a non-empty string')
24
+ }
25
+
26
+ if (!renderer) {
27
+ throw new Error('Renderer is required')
28
+ }
29
+
30
+ // 检查 renderer 是否有 render 属性
31
+ if (!('render' in renderer) || typeof renderer.render !== 'function') {
32
+ throw new Error('Renderer must have a render function')
33
+ }
34
+
35
+ this.renderers.set(type, renderer)
36
+ }
37
+
38
+ /**
39
+ * 批量注册渲染器
40
+ * @param renderers 渲染器映射表
41
+ */
42
+ registerMany(renderers: Record<string, MessageRenderer>): void {
43
+ Object.entries(renderers).forEach(([type, renderer]) => {
44
+ this.register(type, renderer)
45
+ })
46
+ }
47
+
48
+ /**
49
+ * 获取指定类型的渲染器
50
+ * @param type 消息内容类型
51
+ * @returns 渲染器实现,如果未注册则返回 undefined
52
+ */
53
+ get(type: string): MessageRenderer | undefined {
54
+ return this.renderers.get(type)
55
+ }
56
+
57
+ /**
58
+ * 检查是否已注册指定类型的渲染器
59
+ * @param type 消息内容类型
60
+ * @returns 是否已注册
61
+ */
62
+ has(type: string): boolean {
63
+ return this.renderers.has(type)
64
+ }
65
+
66
+ /**
67
+ * 移除指定类型的渲染器
68
+ * @param type 消息内容类型
69
+ * @returns 是否成功移除
70
+ */
71
+ unregister(type: string): boolean {
72
+ return this.renderers.delete(type)
73
+ }
74
+
75
+ /**
76
+ * 清空所有已注册的渲染器
77
+ */
78
+ clear(): void {
79
+ this.renderers.clear()
80
+ }
81
+
82
+ /**
83
+ * 渲染消息内容
84
+ * @param content 消息内容
85
+ * @param isUser 是否为用户消息
86
+ * @param isSystem 是否为系统消息
87
+ * @returns 渲染结果,如果未找到对应渲染器则返回 null
88
+ */
89
+ render(content: MessageContent, isUser: boolean, isSystem: boolean): React.ReactNode {
90
+ const renderer = this.renderers.get(content.type)
91
+
92
+ if (!renderer) {
93
+ console.warn(`[MessageRendererRegistry] No renderer found for type: ${content.type}`)
94
+ return null
95
+ }
96
+
97
+ try {
98
+ return renderer.render(content, isUser, isSystem)
99
+ } catch (error) {
100
+ console.error(`[MessageRendererRegistry] Error rendering ${content.type}:`, error)
101
+ return null
102
+ }
103
+ }
104
+
105
+ /**
106
+ * 获取所有已注册的消息类型
107
+ * @returns 消息类型数组
108
+ */
109
+ getRegisteredTypes(): string[] {
110
+ return Array.from(this.renderers.keys())
111
+ }
112
+ }
113
+
114
+ /**
115
+ * 创建默认的渲染器注册表实例
116
+ * @returns 新的注册表实例
117
+ */
118
+ export function createRendererRegistry(): MessageRendererRegistry {
119
+ return new MessageRendererRegistry()
120
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * 产品数据转换工具函数
3
+ * 将后端返回的产品数据转换为前端使用的格式
4
+ */
5
+
6
+ import type { Product, Variant } from '../types'
7
+
8
+ /**
9
+ * 转换单个变体数据
10
+ */
11
+ function transformVariant(variant: any): Variant {
12
+ return {
13
+ id: variant.shopify_variant_id || variant.id,
14
+ title: variant.title || variant.option1,
15
+ sku: variant.sku,
16
+ price: variant.price
17
+ ? {
18
+ amount: variant.price,
19
+ currency: variant.currency || 'USD',
20
+ }
21
+ : undefined,
22
+ availableForSale: variant.available,
23
+ discount: variant.discount
24
+ ? {
25
+ has_discount: variant.discount.has_discount,
26
+ discount_price: variant.discount.discount_price,
27
+ discount_code: variant.discount.discount_code,
28
+ discount_percentage: variant.discount.discount_percentage,
29
+ discount_type: variant.discount.discount_type,
30
+ discount_value: variant.discount.discount_value,
31
+ }
32
+ : undefined,
33
+ memberPrice: variant.member_price
34
+ ? {
35
+ has_member_price: variant.member_price.has_member_price,
36
+ price: variant.member_price.price,
37
+ }
38
+ : undefined,
39
+ inventoryQuantity: variant.inventory_quantity,
40
+ option1: variant.option1,
41
+ option2: variant.option2,
42
+ option3: variant.option3,
43
+ }
44
+ }
45
+
46
+ /**
47
+ * 转换产品数据
48
+ * 将后端返回的产品对象转换为前端使用的 Product 类型
49
+ *
50
+ * @param product - 后端返回的产品数据
51
+ * @param site - 站点域名,用于构建产品 URL(可选,默认为空字符串)
52
+ * @returns 转换后的 Product 对象
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * const backendProduct = {
57
+ * shopify_product_id: "gid://shopify/Product/123",
58
+ * handle: "product-handle",
59
+ * title: "Product Title",
60
+ * price_range: { min: 99.99, max: 149.99, currency: "USD" },
61
+ * // ... 其他字段
62
+ * }
63
+ *
64
+ * const product = transformProduct(backendProduct, "www.example.com")
65
+ * // product.shopifyId === "gid://shopify/Product/123"
66
+ * // product.productUrl === "https://www.example.com/products/product-handle"
67
+ * ```
68
+ */
69
+ export function transformProduct(product: any, site: string = ''): Product {
70
+ return {
71
+ // 基本信息
72
+ shopifyId: product.shopify_product_id || product.id,
73
+ sku: product.variants?.[0]?.sku || '',
74
+ handle: product.handle,
75
+ title: product.title,
76
+ description: product.description,
77
+ vendor: product.vendor,
78
+
79
+ // 价格信息
80
+ price: {
81
+ amount: product.price_range?.min || product.variants?.[0]?.price || 0,
82
+ currency: product.price_range?.currency || product.variants?.[0]?.currency || 'USD',
83
+ },
84
+ priceRange: product.price_range
85
+ ? {
86
+ min: product.price_range.min,
87
+ max: product.price_range.max,
88
+ currency: product.price_range.currency,
89
+ }
90
+ : undefined,
91
+ memberPriceRange: product.member_price_range
92
+ ? {
93
+ min: product.member_price_range.min,
94
+ max: product.member_price_range.max,
95
+ currency: product.member_price_range.currency,
96
+ }
97
+ : undefined,
98
+
99
+ // 媒体和链接
100
+ imageUrl: product.featured_image || '',
101
+ productUrl: site ? `https://${site}/products/${product.handle}` : `/products/${product.handle}`,
102
+
103
+ // 库存和状态
104
+ stockStatus: product.variants?.[0]?.available ? 'in_stock' : 'out_of_stock',
105
+
106
+ // 评分和热度
107
+ hotScore: product.popularity_score,
108
+ averageRating: product.average_rating,
109
+ reviewCount: product.review_count,
110
+
111
+ // 变体信息
112
+ variantCount: product.variant_count,
113
+ availableCount: product.available_count,
114
+ variants: product.variants?.map(transformVariant),
115
+
116
+ // 特性和标签
117
+ features: product.features
118
+ ? {
119
+ is_new: product.features.is_new,
120
+ has_rental: product.features.has_rental,
121
+ has_presale: product.features.has_presale,
122
+ has_member_price: product.features.has_member_price,
123
+ has_discount: product.features.has_discount,
124
+ }
125
+ : undefined,
126
+ tags: product.tags,
127
+ }
128
+ }
129
+
130
+ /**
131
+ * 批量转换产品列表
132
+ *
133
+ * @param products - 后端返回的产品数组
134
+ * @param site - 站点域名(可选)
135
+ * @returns 转换后的 Product 数组
136
+ *
137
+ * @example
138
+ * ```ts
139
+ * const backendProducts = [product1, product2, product3]
140
+ * const products = transformProducts(backendProducts, "www.example.com")
141
+ * ```
142
+ */
143
+ export function transformProducts(products: any[], site?: string): Product[] {
144
+ if (!Array.isArray(products)) {
145
+ return []
146
+ }
147
+
148
+ return products.map(product => transformProduct(product, site))
149
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * userId 生成和管理工具
3
+ * 基于 specs/livechat-widget/research.md 的决策
4
+ *
5
+ * 策略:
6
+ * 1. 优先从 localStorage 读取
7
+ * 2. 尝试获取 Google Analytics ID (GAID)
8
+ * 3. 兜底:时间戳 + 随机数哈希
9
+ */
10
+
11
+ const STORAGE_KEY = 'livechat_user_id'
12
+
13
+ /**
14
+ * 获取用户唯一标识符(异步版本)
15
+ * @returns userId (GAID 或哈希值)
16
+ */
17
+ export async function getUserId(): Promise<string> {
18
+ // 1. 尝试从 localStorage 读取
19
+ if (typeof window !== 'undefined') {
20
+ const stored = localStorage.getItem(STORAGE_KEY)
21
+ if (stored) return stored
22
+ }
23
+
24
+ // 2. 尝试获取 GAID
25
+ const gaid = getGAID()
26
+ if (gaid) {
27
+ if (typeof window !== 'undefined') {
28
+ localStorage.setItem(STORAGE_KEY, gaid)
29
+ }
30
+ return gaid
31
+ }
32
+
33
+ // 3. 兜底:使用 SHA-256 生成哈希
34
+ try {
35
+ const fallback = await generateHashedUserId()
36
+ if (typeof window !== 'undefined') {
37
+ localStorage.setItem(STORAGE_KEY, fallback)
38
+ }
39
+ return fallback
40
+ } catch (error) {
41
+ // 如果 SHA-256 失败,使用同步兜底方案
42
+ console.warn('[LiveChat userId] SHA-256 failed, using sync fallback:', error)
43
+ const fallback = generateHashedUserIdSync()
44
+ if (typeof window !== 'undefined') {
45
+ localStorage.setItem(STORAGE_KEY, fallback)
46
+ }
47
+ return fallback
48
+ }
49
+ }
50
+
51
+ /**
52
+ * 从 Google Analytics 获取 Client ID (GAID)
53
+ * @returns GAID 或 null
54
+ */
55
+ function getGAID(): string | null {
56
+ if (typeof window === 'undefined') return null
57
+
58
+ // 检查 gtag 是否可用
59
+ if (typeof (window as any).gtag !== 'function') {
60
+ console.warn('[LiveChat userId] Google Analytics gtag is not available')
61
+ return null
62
+ }
63
+
64
+ try {
65
+ // Google Analytics 4 方法
66
+ // 注意:gtag 的 'get' 命令是异步的,这里使用同步 fallback
67
+ // 在实际项目中,如果 GA 已初始化,可以从 dataLayer 读取
68
+ const dataLayer = (window as any).dataLayer || []
69
+
70
+ // 尝试从 dataLayer 中查找已存在的 client_id
71
+ for (const item of dataLayer) {
72
+ if (item && item[1] && typeof item[1] === 'object') {
73
+ const clientId = item[1].client_id || item[1].clientId
74
+ if (clientId && typeof clientId === 'string') {
75
+ return `G-${clientId}`
76
+ }
77
+ }
78
+ }
79
+
80
+ // 如果没有找到,返回 null,使用兜底方案
81
+ return null
82
+ } catch (error) {
83
+ console.error('[LiveChat userId] Failed to get GAID:', error)
84
+ return null
85
+ }
86
+ }
87
+
88
+ /**
89
+ * 生成哈希用户 ID(兜底方案)
90
+ * @returns 格式为 user-{hash} 的用户 ID,hash 由 timestamp + random 通过 SHA-256 生成
91
+ */
92
+ async function generateHashedUserId(): Promise<string> {
93
+ const timestamp = Date.now()
94
+ const random = Math.random().toString(36).substring(2, 15) // 生成随机字符串
95
+
96
+ // 将 timestamp 和 random 组合成字符串
97
+ const rawString = `${timestamp}${random}`
98
+
99
+ // 使用 Web Crypto API 生成 SHA-256 哈希
100
+ const encoder = new TextEncoder()
101
+ const data = encoder.encode(rawString)
102
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data)
103
+
104
+ // 将 ArrayBuffer 转换为 16 进制字符串
105
+ const hashArray = Array.from(new Uint8Array(hashBuffer))
106
+ const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
107
+
108
+ // 取前 16 位作为用户 ID(保持简洁)
109
+ return `user-${hashHex.substring(0, 16)}`
110
+ }
111
+
112
+ /**
113
+ * 同步版本的哈希用户 ID 生成(兜底的兜底)
114
+ * 当 Web Crypto API 不可用时使用
115
+ */
116
+ function generateHashedUserIdSync(): string {
117
+ const timestamp = Date.now()
118
+ const random = Math.random().toString(36).substring(2, 15)
119
+ const rawString = `${timestamp}${random}`
120
+
121
+ // 使用简单的哈希算法作为兜底
122
+ let hash = 0
123
+ for (let i = 0; i < rawString.length; i++) {
124
+ const char = rawString.charCodeAt(i)
125
+ hash = (hash << 5) - hash + char
126
+ hash = hash & hash
127
+ }
128
+
129
+ const hashString = Math.abs(hash).toString(16).padStart(8, '0')
130
+ return `user-${hashString}`
131
+ }
132
+
133
+ /**
134
+ * 清除保存的 userId(用于测试或重置)
135
+ */
136
+ export function clearUserId(): void {
137
+ if (typeof window !== 'undefined') {
138
+ localStorage.removeItem(STORAGE_KEY)
139
+ }
140
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * 输入验证工具
3
+ * 基于 specs/livechat-widget/research.md 的 XSS 防护策略
4
+ */
5
+
6
+ /**
7
+ * 清理用户输入
8
+ * - 移除控制字符
9
+ * - 限制长度
10
+ * @param input 用户输入的字符串
11
+ * @param maxLength 最大长度
12
+ * @returns 清理后的字符串
13
+ */
14
+ export function sanitizeInput(input: string, maxLength: number = 5000): string {
15
+ if (!input || typeof input !== 'string') {
16
+ return ''
17
+ }
18
+
19
+ // 移除控制字符(ASCII 0-31 和 127)
20
+ let sanitized = input.replace(/[\x00-\x1F\x7F]/g, '')
21
+
22
+ // 限制长度
23
+ if (sanitized.length > maxLength) {
24
+ sanitized = sanitized.substring(0, maxLength)
25
+ }
26
+
27
+ // 去除首尾空白
28
+ return sanitized.trim()
29
+ }
30
+
31
+ /**
32
+ * 验证 URL 是否安全
33
+ * @param url URL 字符串
34
+ * @returns 是否为安全的 URL
35
+ */
36
+ export function isValidUrl(url: string): boolean {
37
+ if (!url || typeof url !== 'string') {
38
+ return false
39
+ }
40
+
41
+ try {
42
+ const parsed = new URL(url)
43
+ // 只允许 http 和 https 协议
44
+ return parsed.protocol === 'http:' || parsed.protocol === 'https:'
45
+ } catch {
46
+ return false
47
+ }
48
+ }
49
+
50
+ /**
51
+ * 验证 sessionId 是否为有效的 UUID
52
+ * @param sessionId 会话 ID
53
+ * @returns 是否为有效的 UUID
54
+ */
55
+ export function isValidUUID(sessionId: string): boolean {
56
+ if (!sessionId || typeof sessionId !== 'string') {
57
+ return false
58
+ }
59
+
60
+ // UUID v4 格式:xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
61
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
62
+ return uuidRegex.test(sessionId)
63
+ }
64
+
65
+ /**
66
+ * 验证消息内容是否有效
67
+ * @param content 消息内容
68
+ * @returns 是否有效
69
+ */
70
+ export function isValidMessageContent(content: string): boolean {
71
+ if (!content || typeof content !== 'string') {
72
+ return false
73
+ }
74
+
75
+ // 去除空白后不能为空
76
+ const trimmed = content.trim()
77
+ return trimmed.length > 0 && trimmed.length <= 5000
78
+ }
79
+
80
+ /**
81
+ * 转义 HTML 特殊字符
82
+ * @param text 原始文本
83
+ * @returns 转义后的文本
84
+ */
85
+ export function escapeHtml(text: string): string {
86
+ if (!text || typeof text !== 'string') {
87
+ return ''
88
+ }
89
+
90
+ const map: Record<string, string> = {
91
+ '&': '&amp;',
92
+ '<': '&lt;',
93
+ '>': '&gt;',
94
+ '"': '&quot;',
95
+ "'": '&#39;',
96
+ }
97
+
98
+ return text.replace(/[&<>"']/g, char => map[char] || char)
99
+ }