@salla.sa/twilight-components 3.0.24 → 3.0.25

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 (370) hide show
  1. package/dist/cjs/{Helper-sq6ztayx.js → Helper-BSFw7uVh.js} +1 -1
  2. package/dist/cjs/{filepond-BMxlEH8M.js → filepond-DzJcpvBM.js} +1 -1
  3. package/dist/cjs/{filepond-plugin-file-poster-DVSz4StJ.js → filepond-plugin-file-poster-BILkbyw_.js} +1 -1
  4. package/dist/cjs/{filepond-plugin-file-validate-size-CXJiLSzt.js → filepond-plugin-file-validate-size-DIJUAZji.js} +1 -1
  5. package/dist/cjs/{filepond-plugin-file-validate-type-DWK85msl.js → filepond-plugin-file-validate-type-4lpOf3hf.js} +1 -1
  6. package/dist/cjs/{filepond-plugin-image-edit-BHttJI5X.js → filepond-plugin-image-edit-DtE3Wj_4.js} +1 -1
  7. package/dist/cjs/{filepond-plugin-image-exif-orientation-DTKIO2zE.js → filepond-plugin-image-exif-orientation-Dc_jwifJ.js} +1 -1
  8. package/dist/cjs/{filepond-plugin-image-preview-BaRxwhFa.js → filepond-plugin-image-preview-D_8qt0dd.js} +1 -1
  9. package/dist/cjs/{index-BJ3-fFSF.js → index-D0d25H-r.js} +2 -2
  10. package/dist/cjs/{intlTelInput-DNoOb73M.js → intlTelInput-DqmuleM6.js} +1 -1
  11. package/dist/cjs/loader.cjs.js +2 -2
  12. package/dist/cjs/salla-accordion-body_2.cjs.entry.js +1 -1
  13. package/dist/cjs/salla-accordion_6.cjs.entry.js +2 -2
  14. package/dist/cjs/salla-add-product-button_5.cjs.entry.js +2 -2
  15. package/dist/cjs/salla-advertisement.cjs.entry.js +1 -1
  16. package/dist/cjs/salla-alert_2.cjs.entry.js +1 -1
  17. package/dist/cjs/salla-app-install-alert.cjs.entry.js +1 -1
  18. package/dist/cjs/salla-applied-filters.cjs.entry.js +2 -2
  19. package/dist/cjs/salla-apps-icons.cjs.entry.js +1 -1
  20. package/dist/cjs/salla-attachment-view_9.cjs.entry.js +11 -11
  21. package/dist/cjs/salla-badge.cjs.entry.js +1 -1
  22. package/dist/cjs/salla-basket-gap.cjs.entry.js +1 -1
  23. package/dist/cjs/salla-bullet-delivery_3.cjs.entry.js +1 -1
  24. package/dist/cjs/salla-cart-coupons.cjs.entry.js +1 -1
  25. package/dist/cjs/salla-cart-item-offers_2.cjs.entry.js +5 -1
  26. package/dist/cjs/salla-cart-summary-card.cjs.entry.js +1 -1
  27. package/dist/cjs/salla-cashback-banner.cjs.entry.js +1 -1
  28. package/dist/cjs/salla-comment-factors.cjs.entry.js +1 -1
  29. package/dist/cjs/salla-comment-form_9.cjs.entry.js +2 -2
  30. package/dist/cjs/salla-conditional-offer.cjs.entry.js +628 -19
  31. package/dist/cjs/salla-contacts.cjs.entry.js +1 -1
  32. package/dist/cjs/salla-cookies-bar.cjs.entry.js +1 -1
  33. package/dist/cjs/salla-count-down.cjs.entry.js +1 -1
  34. package/dist/cjs/salla-custom-fields.cjs.entry.js +1 -1
  35. package/dist/cjs/salla-delivery-promise.cjs.entry.js +1 -1
  36. package/dist/cjs/salla-edit-order-button.cjs.entry.js +1 -1
  37. package/dist/cjs/salla-filters-widget.cjs.entry.js +2 -2
  38. package/dist/cjs/salla-filters.cjs.entry.js +2 -2
  39. package/dist/cjs/salla-free-shipping-banner.cjs.entry.js +1 -1
  40. package/dist/cjs/salla-free-shipping-tag.cjs.entry.js +1 -1
  41. package/dist/cjs/salla-fulfillment-methods.cjs.entry.js +1 -1
  42. package/dist/cjs/salla-gift-card-redeem_2.cjs.entry.js +1 -1
  43. package/dist/cjs/salla-gifting.cjs.entry.js +1 -1
  44. package/dist/cjs/salla-hook.cjs.entry.js +1 -1
  45. package/dist/cjs/salla-infinite-scroll.cjs.entry.js +2 -2
  46. package/dist/cjs/salla-installment.cjs.entry.js +1 -1
  47. package/dist/cjs/salla-list-tile.cjs.entry.js +1 -1
  48. package/dist/cjs/salla-localization-modal.cjs.entry.js +1 -1
  49. package/dist/cjs/salla-login-modal.cjs.entry.js +1 -1
  50. package/dist/cjs/salla-loyalty-banner_10.cjs.entry.js +1 -1
  51. package/dist/cjs/salla-loyalty-exchange-product-card.cjs.entry.js +1 -1
  52. package/dist/cjs/salla-loyalty-points-banner.cjs.entry.js +1 -1
  53. package/dist/cjs/salla-loyalty-points-history.cjs.entry.js +1 -1
  54. package/dist/cjs/salla-loyalty-prize-item.cjs.entry.js +1 -1
  55. package/dist/cjs/salla-loyalty.cjs.entry.js +1 -1
  56. package/dist/cjs/salla-maintenance-alert.cjs.entry.js +1 -1
  57. package/dist/cjs/salla-map_3.cjs.entry.js +1 -1
  58. package/dist/cjs/salla-menu.cjs.entry.js +1 -1
  59. package/dist/cjs/salla-metadata.cjs.entry.js +1 -1
  60. package/dist/cjs/salla-mobile-update-modal.cjs.entry.js +1 -1
  61. package/dist/cjs/salla-multiple-bundle-product-cart_2.cjs.entry.js +1 -1
  62. package/dist/cjs/salla-multiple-bundle-product-options-modal_2.cjs.entry.js +1 -1
  63. package/dist/cjs/salla-multiple-bundle-product.cjs.entry.js +1 -1
  64. package/dist/cjs/salla-next-order-coupon.cjs.entry.js +8 -1
  65. package/dist/cjs/salla-notification-item_2.cjs.entry.js +2 -2
  66. package/dist/cjs/salla-offer-modal.cjs.entry.js +1 -1
  67. package/dist/cjs/salla-offer.cjs.entry.js +1 -1
  68. package/dist/cjs/salla-order-branch.cjs.entry.js +1 -1
  69. package/dist/cjs/salla-order-cancel.cjs.entry.js +1 -1
  70. package/dist/cjs/salla-order-details-multiple-bundle-product.cjs.entry.js +1 -1
  71. package/dist/cjs/salla-order-details-options.cjs.entry.js +1 -1
  72. package/dist/cjs/salla-order-details.cjs.entry.js +1 -1
  73. package/dist/cjs/salla-order-edit-item.cjs.entry.js +1 -1
  74. package/dist/cjs/salla-order-edit-product-card.cjs.entry.js +1 -1
  75. package/dist/cjs/salla-order-edit.cjs.entry.js +1 -1
  76. package/dist/cjs/salla-order-items.cjs.entry.js +1 -1
  77. package/dist/cjs/salla-order-options-modal.cjs.entry.js +1 -1
  78. package/dist/cjs/salla-order-payment.cjs.entry.js +1 -1
  79. package/dist/cjs/salla-order-rewards.cjs.entry.js +1 -1
  80. package/dist/cjs/salla-order-shipments.cjs.entry.js +2 -11
  81. package/dist/cjs/salla-order-summary.cjs.entry.js +1 -1
  82. package/dist/cjs/salla-order-totals-card.cjs.entry.js +1 -1
  83. package/dist/cjs/salla-orders.cjs.entry.js +2 -2
  84. package/dist/cjs/salla-payments.cjs.entry.js +1 -1
  85. package/dist/cjs/salla-placeholder.cjs.entry.js +1 -1
  86. package/dist/cjs/salla-price-range.cjs.entry.js +1 -1
  87. package/dist/cjs/salla-product-card-embed.cjs.entry.js +1 -1
  88. package/dist/cjs/salla-product-card.cjs.entry.js +1 -1
  89. package/dist/cjs/salla-product-size-guide.cjs.entry.js +1 -1
  90. package/dist/cjs/salla-products-list.cjs.entry.js +2 -2
  91. package/dist/cjs/salla-products-slider.cjs.entry.js +2 -2
  92. package/dist/cjs/salla-quick-order.cjs.entry.js +1 -1
  93. package/dist/cjs/salla-rating-modal-v2.cjs.entry.js +2 -2
  94. package/dist/cjs/salla-rating-modal.cjs.entry.js +2 -2
  95. package/dist/cjs/salla-review-factors-tags.cjs.entry.js +1 -1
  96. package/dist/cjs/salla-review-order-item.cjs.entry.js +1 -1
  97. package/dist/cjs/salla-scopes.cjs.entry.js +1 -1
  98. package/dist/cjs/salla-search-product-card.cjs.entry.js +1 -1
  99. package/dist/cjs/salla-search.cjs.entry.js +2 -2
  100. package/dist/cjs/salla-skeleton.cjs.entry.js +1 -1
  101. package/dist/cjs/salla-social-share.cjs.entry.js +1 -1
  102. package/dist/cjs/salla-social.cjs.entry.js +1 -1
  103. package/dist/cjs/salla-tab-content_3.cjs.entry.js +1 -1
  104. package/dist/cjs/salla-thank-you-header.cjs.entry.js +1 -1
  105. package/dist/cjs/salla-thank-you-page.cjs.entry.js +1 -1
  106. package/dist/cjs/salla-tiered-offer.cjs.entry.js +1 -1
  107. package/dist/cjs/salla-trust-badges.cjs.entry.js +1 -1
  108. package/dist/cjs/salla-user-menu.cjs.entry.js +1 -1
  109. package/dist/cjs/salla-user-profile.cjs.entry.js +1 -1
  110. package/dist/cjs/salla-user-settings.cjs.entry.js +1 -1
  111. package/dist/cjs/salla-verify.cjs.entry.js +1 -1
  112. package/dist/cjs/salla-wallet.cjs.entry.js +1 -1
  113. package/dist/cjs/salla-wishlist-actions.cjs.entry.js +1 -1
  114. package/dist/cjs/salla-wishlist-summary.cjs.entry.js +2 -2
  115. package/dist/cjs/twilight.cjs.js +2 -2
  116. package/dist/cjs/{vanilla-picker-CceY3g8c.js → vanilla-picker-DP9Hy8IR.js} +1 -1
  117. package/dist/collection/components/salla-cart-item-offers/salla-cart-item-offers.js +4 -0
  118. package/dist/collection/components/salla-conditional-offer/salla-conditional-offer.css +139 -0
  119. package/dist/collection/components/salla-conditional-offer/salla-conditional-offer.js +568 -14
  120. package/dist/collection/components/salla-next-order-coupon/salla-next-order-coupon.js +7 -0
  121. package/dist/collection/components/salla-order-shipments/salla-order-shipments.js +1 -10
  122. package/dist/collection/shared/cart-free-gifts.js +42 -1
  123. package/dist/collection/shared/offer-stages.js +22 -3
  124. package/dist/components/index.js +2 -2
  125. package/dist/components/salla-cart-item-offers.js +4 -0
  126. package/dist/components/salla-conditional-offer.js +631 -18
  127. package/dist/components/salla-next-order-coupon.js +7 -0
  128. package/dist/components/salla-order-shipments.js +1 -10
  129. package/dist/docs.json +2 -2
  130. package/dist/esm/{Helper-BwzPfwZJ.js → Helper-DmnUAusv.js} +1 -1
  131. package/dist/esm/{filepond-JNV-1zqR.js → filepond-DZbzdjkK.js} +1 -1
  132. package/dist/esm/{filepond-plugin-file-poster-nFqNHCag.js → filepond-plugin-file-poster-D9cjvoj-.js} +1 -1
  133. package/dist/esm/{filepond-plugin-file-validate-size-P3KmOTH8.js → filepond-plugin-file-validate-size-D-kjllC-.js} +1 -1
  134. package/dist/esm/{filepond-plugin-file-validate-type-BV0Ctr7U.js → filepond-plugin-file-validate-type-D9hsNaJl.js} +1 -1
  135. package/dist/esm/{filepond-plugin-image-edit-D62rKlfH.js → filepond-plugin-image-edit-CDHAxJY8.js} +1 -1
  136. package/dist/esm/{filepond-plugin-image-exif-orientation-DerapxIk.js → filepond-plugin-image-exif-orientation-BGKiclwK.js} +1 -1
  137. package/dist/esm/{filepond-plugin-image-preview-BlRd2Ble.js → filepond-plugin-image-preview-BUjvnt8G.js} +1 -1
  138. package/dist/esm/{index-CtoXzf1y.js → index-ze9e0dxM.js} +2 -2
  139. package/dist/esm/{intlTelInput-BZz-u5_w.js → intlTelInput-BkYRB6km.js} +1 -1
  140. package/dist/esm/loader.js +3 -3
  141. package/dist/esm/salla-accordion-body_2.entry.js +1 -1
  142. package/dist/esm/salla-accordion_6.entry.js +2 -2
  143. package/dist/esm/salla-add-product-button_5.entry.js +2 -2
  144. package/dist/esm/salla-advertisement.entry.js +1 -1
  145. package/dist/esm/salla-alert_2.entry.js +1 -1
  146. package/dist/esm/salla-app-install-alert.entry.js +1 -1
  147. package/dist/esm/salla-applied-filters.entry.js +2 -2
  148. package/dist/esm/salla-apps-icons.entry.js +1 -1
  149. package/dist/esm/salla-attachment-view_9.entry.js +11 -11
  150. package/dist/esm/salla-badge.entry.js +1 -1
  151. package/dist/esm/salla-basket-gap.entry.js +1 -1
  152. package/dist/esm/salla-bullet-delivery_3.entry.js +1 -1
  153. package/dist/esm/salla-cart-coupons.entry.js +1 -1
  154. package/dist/esm/salla-cart-item-offers_2.entry.js +5 -1
  155. package/dist/esm/salla-cart-summary-card.entry.js +1 -1
  156. package/dist/esm/salla-cashback-banner.entry.js +1 -1
  157. package/dist/esm/salla-comment-factors.entry.js +1 -1
  158. package/dist/esm/salla-comment-form_9.entry.js +2 -2
  159. package/dist/esm/salla-conditional-offer.entry.js +628 -19
  160. package/dist/esm/salla-contacts.entry.js +1 -1
  161. package/dist/esm/salla-cookies-bar.entry.js +1 -1
  162. package/dist/esm/salla-count-down.entry.js +1 -1
  163. package/dist/esm/salla-custom-fields.entry.js +1 -1
  164. package/dist/esm/salla-delivery-promise.entry.js +1 -1
  165. package/dist/esm/salla-edit-order-button.entry.js +1 -1
  166. package/dist/esm/salla-filters-widget.entry.js +2 -2
  167. package/dist/esm/salla-filters.entry.js +2 -2
  168. package/dist/esm/salla-free-shipping-banner.entry.js +1 -1
  169. package/dist/esm/salla-free-shipping-tag.entry.js +1 -1
  170. package/dist/esm/salla-fulfillment-methods.entry.js +1 -1
  171. package/dist/esm/salla-gift-card-redeem_2.entry.js +1 -1
  172. package/dist/esm/salla-gifting.entry.js +1 -1
  173. package/dist/esm/salla-hook.entry.js +1 -1
  174. package/dist/esm/salla-infinite-scroll.entry.js +2 -2
  175. package/dist/esm/salla-installment.entry.js +1 -1
  176. package/dist/esm/salla-list-tile.entry.js +1 -1
  177. package/dist/esm/salla-localization-modal.entry.js +1 -1
  178. package/dist/esm/salla-login-modal.entry.js +1 -1
  179. package/dist/esm/salla-loyalty-banner_10.entry.js +1 -1
  180. package/dist/esm/salla-loyalty-exchange-product-card.entry.js +1 -1
  181. package/dist/esm/salla-loyalty-points-banner.entry.js +1 -1
  182. package/dist/esm/salla-loyalty-points-history.entry.js +1 -1
  183. package/dist/esm/salla-loyalty-prize-item.entry.js +1 -1
  184. package/dist/esm/salla-loyalty.entry.js +1 -1
  185. package/dist/esm/salla-maintenance-alert.entry.js +1 -1
  186. package/dist/esm/salla-map_3.entry.js +1 -1
  187. package/dist/esm/salla-menu.entry.js +1 -1
  188. package/dist/esm/salla-metadata.entry.js +1 -1
  189. package/dist/esm/salla-mobile-update-modal.entry.js +1 -1
  190. package/dist/esm/salla-multiple-bundle-product-cart_2.entry.js +1 -1
  191. package/dist/esm/salla-multiple-bundle-product-options-modal_2.entry.js +1 -1
  192. package/dist/esm/salla-multiple-bundle-product.entry.js +1 -1
  193. package/dist/esm/salla-next-order-coupon.entry.js +8 -1
  194. package/dist/esm/salla-notification-item_2.entry.js +2 -2
  195. package/dist/esm/salla-offer-modal.entry.js +1 -1
  196. package/dist/esm/salla-offer.entry.js +1 -1
  197. package/dist/esm/salla-order-branch.entry.js +1 -1
  198. package/dist/esm/salla-order-cancel.entry.js +1 -1
  199. package/dist/esm/salla-order-details-multiple-bundle-product.entry.js +1 -1
  200. package/dist/esm/salla-order-details-options.entry.js +1 -1
  201. package/dist/esm/salla-order-details.entry.js +1 -1
  202. package/dist/esm/salla-order-edit-item.entry.js +1 -1
  203. package/dist/esm/salla-order-edit-product-card.entry.js +1 -1
  204. package/dist/esm/salla-order-edit.entry.js +1 -1
  205. package/dist/esm/salla-order-items.entry.js +1 -1
  206. package/dist/esm/salla-order-options-modal.entry.js +1 -1
  207. package/dist/esm/salla-order-payment.entry.js +1 -1
  208. package/dist/esm/salla-order-rewards.entry.js +1 -1
  209. package/dist/esm/salla-order-shipments.entry.js +2 -11
  210. package/dist/esm/salla-order-summary.entry.js +1 -1
  211. package/dist/esm/salla-order-totals-card.entry.js +1 -1
  212. package/dist/esm/salla-orders.entry.js +2 -2
  213. package/dist/esm/salla-payments.entry.js +1 -1
  214. package/dist/esm/salla-placeholder.entry.js +1 -1
  215. package/dist/esm/salla-price-range.entry.js +1 -1
  216. package/dist/esm/salla-product-card-embed.entry.js +1 -1
  217. package/dist/esm/salla-product-card.entry.js +1 -1
  218. package/dist/esm/salla-product-size-guide.entry.js +1 -1
  219. package/dist/esm/salla-products-list.entry.js +2 -2
  220. package/dist/esm/salla-products-slider.entry.js +2 -2
  221. package/dist/esm/salla-quick-order.entry.js +1 -1
  222. package/dist/esm/salla-rating-modal-v2.entry.js +2 -2
  223. package/dist/esm/salla-rating-modal.entry.js +2 -2
  224. package/dist/esm/salla-review-factors-tags.entry.js +1 -1
  225. package/dist/esm/salla-review-order-item.entry.js +1 -1
  226. package/dist/esm/salla-scopes.entry.js +1 -1
  227. package/dist/esm/salla-search-product-card.entry.js +1 -1
  228. package/dist/esm/salla-search.entry.js +2 -2
  229. package/dist/esm/salla-skeleton.entry.js +1 -1
  230. package/dist/esm/salla-social-share.entry.js +1 -1
  231. package/dist/esm/salla-social.entry.js +1 -1
  232. package/dist/esm/salla-tab-content_3.entry.js +1 -1
  233. package/dist/esm/salla-thank-you-header.entry.js +1 -1
  234. package/dist/esm/salla-thank-you-page.entry.js +1 -1
  235. package/dist/esm/salla-tiered-offer.entry.js +1 -1
  236. package/dist/esm/salla-trust-badges.entry.js +1 -1
  237. package/dist/esm/salla-user-menu.entry.js +1 -1
  238. package/dist/esm/salla-user-profile.entry.js +1 -1
  239. package/dist/esm/salla-user-settings.entry.js +1 -1
  240. package/dist/esm/salla-verify.entry.js +1 -1
  241. package/dist/esm/salla-wallet.entry.js +1 -1
  242. package/dist/esm/salla-wishlist-actions.entry.js +1 -1
  243. package/dist/esm/salla-wishlist-summary.entry.js +2 -2
  244. package/dist/esm/twilight.js +3 -3
  245. package/dist/esm/{vanilla-picker-BfmpXY4G.js → vanilla-picker-CxhuwWDi.js} +1 -1
  246. package/dist/twilight/{p-dc2be7b8.entry.js → p-008740fe.entry.js} +1 -1
  247. package/dist/twilight/{p-aac9d2fe.entry.js → p-024ecda2.entry.js} +1 -1
  248. package/dist/twilight/{p-60ae422e.entry.js → p-02b7dfcc.entry.js} +1 -1
  249. package/dist/twilight/{p-4008e164.entry.js → p-03b2df97.entry.js} +1 -1
  250. package/dist/twilight/{p-36167cbe.entry.js → p-07e581af.entry.js} +1 -1
  251. package/dist/twilight/{p-2afc4658.entry.js → p-08be494e.entry.js} +1 -1
  252. package/dist/twilight/{p-4f4933f8.entry.js → p-0c022fbd.entry.js} +1 -1
  253. package/dist/twilight/{p-ebe45961.entry.js → p-0c0b2b36.entry.js} +1 -1
  254. package/dist/twilight/{p-40e651ac.entry.js → p-0fd15433.entry.js} +1 -1
  255. package/dist/twilight/{p-a4fee391.entry.js → p-11eb8675.entry.js} +1 -1
  256. package/dist/twilight/{p-f738f492.entry.js → p-12a52f1b.entry.js} +1 -1
  257. package/dist/twilight/{p-3e285056.entry.js → p-16e2a110.entry.js} +1 -1
  258. package/dist/twilight/{p-3da2e3f3.entry.js → p-1ca39f1c.entry.js} +1 -1
  259. package/dist/twilight/{p-fbce8a18.entry.js → p-20949dfb.entry.js} +1 -1
  260. package/dist/twilight/{p-4080fdc9.entry.js → p-2aa9147c.entry.js} +1 -1
  261. package/dist/twilight/{p-0b583810.entry.js → p-2dcd24fe.entry.js} +1 -1
  262. package/dist/twilight/{p-8413d52b.entry.js → p-331003bc.entry.js} +1 -1
  263. package/dist/twilight/{p-1f39ca4f.entry.js → p-37b64e36.entry.js} +1 -1
  264. package/dist/twilight/{p-1af75625.entry.js → p-3a2ff4d6.entry.js} +1 -1
  265. package/dist/twilight/{p-213368ef.entry.js → p-3ec3e9fb.entry.js} +1 -1
  266. package/dist/twilight/{p-36a7344c.entry.js → p-446e3f38.entry.js} +1 -1
  267. package/dist/twilight/{p-ed37e702.entry.js → p-455a4702.entry.js} +1 -1
  268. package/dist/twilight/{p-e89daada.entry.js → p-45a7d444.entry.js} +1 -1
  269. package/dist/twilight/{p-f3455825.entry.js → p-474d882c.entry.js} +1 -1
  270. package/dist/twilight/{p-b3472096.entry.js → p-47c79c49.entry.js} +1 -1
  271. package/dist/twilight/{p-4254e9c2.entry.js → p-4c191ab4.entry.js} +1 -1
  272. package/dist/twilight/{p-8cbf3bc2.entry.js → p-4cf81a44.entry.js} +1 -1
  273. package/dist/twilight/{p-97065b4f.entry.js → p-4ec15515.entry.js} +1 -1
  274. package/dist/twilight/{p-9c1b0978.entry.js → p-519cf4c2.entry.js} +1 -1
  275. package/dist/twilight/{p-374d9f0c.entry.js → p-57fafaba.entry.js} +1 -1
  276. package/dist/twilight/{p-a9ae6ab9.entry.js → p-59694c45.entry.js} +1 -1
  277. package/dist/twilight/{p-5dea6c09.entry.js → p-5b118b5b.entry.js} +1 -1
  278. package/dist/twilight/{p-7169ba9a.entry.js → p-5ceda3d5.entry.js} +1 -1
  279. package/dist/twilight/{p-02ddd64c.entry.js → p-5d8937c5.entry.js} +1 -1
  280. package/dist/twilight/{p-a8102290.entry.js → p-5e0f85ae.entry.js} +1 -1
  281. package/dist/twilight/{p-b3c9ab0e.entry.js → p-5f9d87ec.entry.js} +1 -1
  282. package/dist/twilight/{p-034b80ad.entry.js → p-61b98119.entry.js} +1 -1
  283. package/dist/twilight/{p-f51f31df.entry.js → p-67cf7454.entry.js} +1 -1
  284. package/dist/twilight/{p-cd55369c.entry.js → p-6d6b99fe.entry.js} +1 -1
  285. package/dist/twilight/{p-1d105a57.entry.js → p-71ce8e4f.entry.js} +1 -1
  286. package/dist/twilight/{p-00a1ec4e.entry.js → p-73649c81.entry.js} +1 -1
  287. package/dist/twilight/{p-a260f35e.entry.js → p-75ad7f14.entry.js} +1 -1
  288. package/dist/twilight/{p-117e714b.entry.js → p-75f7f1d8.entry.js} +1 -1
  289. package/dist/twilight/{p-2e1dd9cf.entry.js → p-770142fe.entry.js} +1 -1
  290. package/dist/twilight/{p-7c684782.entry.js → p-7731e280.entry.js} +1 -1
  291. package/dist/twilight/p-776f9492.entry.js +4 -0
  292. package/dist/twilight/{p-365ff361.entry.js → p-7790a9a1.entry.js} +1 -1
  293. package/dist/twilight/p-78c04993.entry.js +4 -0
  294. package/dist/twilight/{p-3d3eda0f.entry.js → p-792494c8.entry.js} +1 -1
  295. package/dist/twilight/{p-c4264e87.entry.js → p-7a985fe3.entry.js} +1 -1
  296. package/dist/twilight/{p-6495383e.entry.js → p-7fffa509.entry.js} +1 -1
  297. package/dist/twilight/{p-d76b7564.entry.js → p-8077d8b6.entry.js} +1 -1
  298. package/dist/twilight/{p-9e50f29c.entry.js → p-830abcc1.entry.js} +1 -1
  299. package/dist/twilight/{p-3356638d.entry.js → p-89598ad3.entry.js} +1 -1
  300. package/dist/twilight/{p-0cb60522.entry.js → p-8b0b6bca.entry.js} +1 -1
  301. package/dist/twilight/{p-8b9c44c8.entry.js → p-8d096f95.entry.js} +1 -1
  302. package/dist/twilight/{p-a236dd3b.entry.js → p-8d3c5d1f.entry.js} +1 -1
  303. package/dist/twilight/{p-ae1efde9.entry.js → p-93571975.entry.js} +1 -1
  304. package/dist/twilight/{p-38b70281.entry.js → p-97c5b11d.entry.js} +1 -1
  305. package/dist/twilight/{p-c18fe9e5.entry.js → p-97fa91db.entry.js} +1 -1
  306. package/dist/twilight/{p-94d5b080.entry.js → p-9a2567e2.entry.js} +1 -1
  307. package/dist/twilight/p-9acfe389.entry.js +4 -0
  308. package/dist/twilight/{p-79b743eb.entry.js → p-9dd7cf71.entry.js} +1 -1
  309. package/dist/twilight/{p-2882d626.entry.js → p-9fe5c0f5.entry.js} +1 -1
  310. package/dist/twilight/{p-F1Erq7GR.js → p-B8nGVDvQ.js} +1 -1
  311. package/dist/twilight/{p-CC-KFXD9.js → p-BjUh9YCs.js} +1 -1
  312. package/dist/twilight/{p-BwUX8dew.js → p-BlPdaFn2.js} +1 -1
  313. package/dist/twilight/{p-6KZIgnh6.js → p-BpJsOjKP.js} +1 -1
  314. package/dist/twilight/{p-78r0jdyY.js → p-C9ldyQUq.js} +1 -1
  315. package/dist/twilight/{p-x4_Sxqd_.js → p-CICtuB_u.js} +1 -1
  316. package/dist/twilight/{p-DT73iyFr.js → p-CNQaYCUJ.js} +1 -1
  317. package/dist/twilight/{p-DYAn-F48.js → p-DOUAavek.js} +2 -2
  318. package/dist/twilight/{p-b2cbdaa0.entry.js → p-a1a650ab.entry.js} +1 -1
  319. package/dist/twilight/{p-71b5546b.entry.js → p-a7566af2.entry.js} +1 -1
  320. package/dist/twilight/{p-e8198efc.entry.js → p-a7849674.entry.js} +1 -1
  321. package/dist/twilight/{p-2140be84.entry.js → p-a865995d.entry.js} +1 -1
  322. package/dist/twilight/{p-8fe950e8.entry.js → p-b297fe2e.entry.js} +1 -1
  323. package/dist/twilight/{p-e8ebb2fe.entry.js → p-b6098938.entry.js} +1 -1
  324. package/dist/twilight/{p-0d1b8490.entry.js → p-b71673ee.entry.js} +1 -1
  325. package/dist/twilight/{p-e2cb5c49.entry.js → p-bb137dbb.entry.js} +1 -1
  326. package/dist/twilight/{p-6ffbf505.entry.js → p-bd06c200.entry.js} +1 -1
  327. package/dist/twilight/{p-7303ebd1.entry.js → p-be8c42f1.entry.js} +1 -1
  328. package/dist/twilight/{p-3845e427.entry.js → p-bf5b5f52.entry.js} +1 -1
  329. package/dist/twilight/{p-72b0724c.entry.js → p-bf65d2fa.entry.js} +1 -1
  330. package/dist/twilight/{p-d6a3b2d8.entry.js → p-c0301e00.entry.js} +1 -1
  331. package/dist/twilight/{p-2c04bba3.entry.js → p-c122e9c1.entry.js} +1 -1
  332. package/dist/twilight/{p-f67d03fb.entry.js → p-c6f09614.entry.js} +1 -1
  333. package/dist/twilight/{p-8ea3a265.entry.js → p-cb961f05.entry.js} +1 -1
  334. package/dist/twilight/{p-e5dc1d27.entry.js → p-cbd056ba.entry.js} +1 -1
  335. package/dist/twilight/{p-4c5a7ffc.entry.js → p-cdeaf60b.entry.js} +1 -1
  336. package/dist/twilight/{p-D5DrmlyQ.js → p-cykIAOlk.js} +1 -1
  337. package/dist/twilight/{p-2566f688.entry.js → p-d089ac6d.entry.js} +1 -1
  338. package/dist/twilight/{p-ebed682b.entry.js → p-d2396143.entry.js} +1 -1
  339. package/dist/twilight/{p-974c100d.entry.js → p-d300a117.entry.js} +1 -1
  340. package/dist/twilight/{p-1b4c3b85.entry.js → p-d61298f8.entry.js} +1 -1
  341. package/dist/twilight/{p-f664fa69.entry.js → p-d89328d7.entry.js} +1 -1
  342. package/dist/twilight/p-da0349f9.entry.js +4 -0
  343. package/dist/twilight/{p-118aa92e.entry.js → p-da60995f.entry.js} +1 -1
  344. package/dist/twilight/{p-cbff6c68.entry.js → p-dbb4db25.entry.js} +1 -1
  345. package/dist/twilight/{p-4adaee32.entry.js → p-dceb3725.entry.js} +1 -1
  346. package/dist/twilight/{p-8fd6ed52.entry.js → p-de10d0c3.entry.js} +1 -1
  347. package/dist/twilight/{p-77deaa00.entry.js → p-dfde387f.entry.js} +1 -1
  348. package/dist/twilight/{p-7a8fde62.entry.js → p-e23b84bf.entry.js} +1 -1
  349. package/dist/twilight/{p-0ce0cdb0.entry.js → p-e3dd9502.entry.js} +1 -1
  350. package/dist/twilight/{p-b1bc70af.entry.js → p-e849f5ef.entry.js} +1 -1
  351. package/dist/twilight/{p-f310b923.entry.js → p-ed96c703.entry.js} +1 -1
  352. package/dist/twilight/{p-2401f0d7.entry.js → p-f054cf8e.entry.js} +1 -1
  353. package/dist/twilight/{p-aca2717e.entry.js → p-f23ada45.entry.js} +1 -1
  354. package/dist/twilight/{p-95e7c2e1.entry.js → p-f7c06bee.entry.js} +1 -1
  355. package/dist/twilight/{p-c1ab1b40.entry.js → p-f9dd6b3e.entry.js} +1 -1
  356. package/dist/twilight/{p-055bf228.entry.js → p-fb955257.entry.js} +1 -1
  357. package/dist/twilight/{p-061216d4.entry.js → p-ffc7f526.entry.js} +1 -1
  358. package/dist/twilight/{p-BeC8AhMn.js → p-wYYV5ykR.js} +1 -1
  359. package/dist/twilight/{p-CtoXzf1y.js → p-ze9e0dxM.js} +1 -1
  360. package/dist/twilight/twilight.esm.js +1 -1
  361. package/dist/types/components/salla-conditional-offer/interfaces.d.ts +36 -0
  362. package/dist/types/components/salla-conditional-offer/salla-conditional-offer.d.ts +182 -3
  363. package/dist/types/components/salla-order-shipments/salla-order-shipments.d.ts +0 -1
  364. package/dist/types/shared/cart-free-gifts.d.ts +26 -0
  365. package/dist/types/shared/offer-stages.d.ts +6 -1
  366. package/package.json +5 -5
  367. package/dist/twilight/p-0fb390fb.entry.js +0 -4
  368. package/dist/twilight/p-17be7967.entry.js +0 -4
  369. package/dist/twilight/p-5bc2190a.entry.js +0 -4
  370. package/dist/twilight/p-919650b7.entry.js +0 -4
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { Host, h } from "@stencil/core";
5
5
  import { DiscountType, OfferType, } from "./interfaces";
6
- import { getCartFreeGiftItems, getCartItemCount, hasAuthoritativeCartData, refreshCartFreeGifts, } from "../../shared/cart-free-gifts";
6
+ import { getCartFreeGiftItems, getCartItemCount, getCartProductLines, hasAuthoritativeCartData, refreshCartFreeGifts, } from "../../shared/cart-free-gifts";
7
7
  import { CART_UPDATE_COALESCE_MS } from "./cart-update-window";
8
8
  import { NO_STAGE_REACHED, OfferStageMeasure, computeOfferStageProgress, getRemainingToStage, isUsableThreshold, sortByThreshold, } from "../../shared/offer-stages";
9
9
  /** Root of every translation key this component uses. */
@@ -69,6 +69,19 @@ const RELOCATION_ANCESTOR_LIMIT = 3;
69
69
  * DOM with no cart in it. Past this deadline the full-width banner is the final answer.
70
70
  */
71
71
  const RELOCATION_WATCH_MS = 5000;
72
+ /**
73
+ * CP-2171 — how long the picker stays locked after an add succeeded when no fresh cart
74
+ * lines arrive (a missed `cart::updated`). Long enough to cover the coalescing window and the
75
+ * cart lookup; short enough that a lost event never leaves Add ignoring taps for long. Past it
76
+ * the lock goes and the lines are read again — see `unlockOnStaleLines`.
77
+ */
78
+ const FREE_PRODUCT_UNLOCK_FALLBACK_MS = 5000;
79
+ /** CP-2171 — how long the cart page fetch for new lines may take before falling back to a reload. */
80
+ const CART_PAGE_FETCH_TIMEOUT_MS = 8000;
81
+ /** `sessionStorage` key of the cart page reload guard (see `reloadForMissingCartLines`). */
82
+ const CART_PAGE_RELOAD_KEY = 'salla-conditional-offer:reloaded-for';
83
+ /** A theme's cart line forms. Product option forms (`.cart-options`) carry an `item-` id too, but are not lines. */
84
+ const CART_LINE_FORMS = 'form[id^="item-"]:not(.cart-options form)';
72
85
  /**
73
86
  * Clear space kept between the ⓘ popover and the viewport edge when it is nudged back
74
87
  * inside. Purely cosmetic — one gutter, both edges, both directions.
@@ -101,10 +114,32 @@ const EN_FALLBACKS = {
101
114
  details_title: 'Offer details',
102
115
  free_gift: 'Free Gift',
103
116
  free_products_title: 'Free products selected from the offer',
117
+ // CP-5033 — the gift tier is reached but none of its gifts can be added.
118
+ free_products_unavailable: 'The free products are currently unavailable',
119
+ // CP-2171 — the free product picker.
120
+ free_products_choose: 'Choose :count free products',
121
+ // Its own key, not :count = 2: Arabic uses the dual form, with no number.
122
+ free_products_choose_two: 'Choose 2 free products',
123
+ free_product_choose_one: 'Choose your free product',
124
+ // The banner line at the gift tier: "…Choose it/them now" while no gift is picked yet, then
125
+ // the plain line once one is. Same one / two / :count split as the picker title.
126
+ free_product_got_one: 'You got a free product',
127
+ free_product_got_pick_one: 'You got a free product! Choose it now',
128
+ free_products_got: 'You got :count free products',
129
+ free_products_got_pick: 'You got :count free products! Choose them now',
130
+ free_products_got_pick_two: 'You got 2 free products! Choose them now',
131
+ free_products_got_two: 'You got 2 free products',
132
+ // A picker tier's reward in "Add :remaining and get :reward" and the tooltip rows, instead of
133
+ // the server's count-less "Free Gift". Same one / two / :count split.
134
+ free_product_reward_one: 'a free product',
135
+ free_products_reward: ':count free products',
136
+ free_products_reward_two: '2 free products',
104
137
  guidance_conditions_unmet: 'Your cart doesn’t meet the conditions for the next reward yet',
105
138
  guidance_next: 'Add :remaining and get :reward',
106
139
  guidance_next_generic: 'Add :remaining to unlock the next reward',
107
140
  manage_offer: 'Manage offer',
141
+ // The picker's toggle. Its own key, so offers without a picker keep master's "Manage offer".
142
+ offer_toggle: 'Offer',
108
143
  tier_requirement: 'Spend :threshold and get :reward',
109
144
  tier_requirement_count: 'Buy :threshold and get :reward',
110
145
  tier_requirement_count_generic: 'Buy :threshold to unlock this offer',
@@ -114,6 +149,15 @@ const EN_FALLBACKS = {
114
149
  /** The gift price label reuses the platform's existing "free of charge" string. */
115
150
  const FREE_LABEL_KEY = 'pages.checkout.free_charge';
116
151
  const FREE_LABEL_FALLBACK = 'Free';
152
+ /**
153
+ * CP-2171 — the picker's add label is the platform's shared "Add", not a cart-offer key of its
154
+ * own: the dictionaries keep one string per meaning.
155
+ */
156
+ const ADD_LABEL_KEY = 'common.elements.add';
157
+ const ADD_LABEL_FALLBACK = 'Add';
158
+ // The "Free products" panel title reuses the loyalty program's existing "Free products".
159
+ const FREE_PRODUCTS_TITLE_KEY = 'pages.loyalty_program.free_product_title';
160
+ const FREE_PRODUCTS_TITLE_FALLBACK = 'Free products';
117
161
  /**
118
162
  * Escape a merchant-controlled string before it is substituted into a translation
119
163
  * template that we then render with `innerHTML`.
@@ -219,6 +263,37 @@ export class SallaConditionalOffer {
219
263
  this.canRender = true;
220
264
  /** Free-gift cart LINES belonging to this offer — the "ادارة العرض" card. */
221
265
  this.gifts = [];
266
+ /** CP-2171 — every cart line (paid and free), for the picker's per-product state. */
267
+ this.cartLines = [];
268
+ /**
269
+ * CP-2171 — whether `cartLines` came from a cart lookup that answered. Until then the
270
+ * lines are `[]`: the "Free products" panel and the gift-pick banner line wait for it.
271
+ */
272
+ this.cartLinesLoaded = false;
273
+ /** CP-2171 — the picker's product cards, keyed by ENCODED product id. */
274
+ this.freeProductCards = {};
275
+ /**
276
+ * CP-2171 — the product (encoded id) an add is in flight for. Held until cart lines read
277
+ * AFTER the add arrive, not just until the request ends: the cart refresh starts only after
278
+ * the coalescing window, and a tap in between is ignored, so a double tap adds one unit.
279
+ */
280
+ this.pendingFreeProduct = null;
281
+ /** `giftsGeneration` when the pending change succeeded; a cart load started after it unlocks. */
282
+ this.pendingFreeProductSettledAt = null;
283
+ this.pendingFreeProductTimer = null;
284
+ /**
285
+ * `giftsGeneration` when an add from the picker succeeded: the first cart lines read after it
286
+ * are checked against the page (see `showMissingCartLines`). Kept apart from the
287
+ * button lock on purpose: the lock's fallback can fire before those lines arrive, and the
288
+ * page still has to show the new line when they do.
289
+ */
290
+ this.cartPageCheckAfter = null;
291
+ /** A cart page fetch for missing lines is in flight; lines read meanwhile wait in `pendingCartPageLines`. */
292
+ this.insertingCartLines = false;
293
+ this.pendingCartPageLines = null;
294
+ /** Re-reads of the cart after a failed lookup while a picker change waits; see `retryPickerCartLookup`. */
295
+ this.pickerLookupRetries = 0;
296
+ this.pickerLookupTimer = null;
222
297
  /**
223
298
  * How many LINES the cart holds — `null` while that is genuinely UNKNOWN.
224
299
  *
@@ -484,6 +559,9 @@ export class SallaConditionalOffer {
484
559
  clearTimeout(this.cartRefreshTimer);
485
560
  this.cartRefreshTimer = null;
486
561
  }
562
+ // A dead host has no picker to unlock; drop the fallback timer with the rest.
563
+ this.unlockFreeProduct();
564
+ this.clearPickerLookupRetry();
487
565
  // Likewise: the timer goes, `cartLookupRetryPending` stays.
488
566
  if (this.cartLookupRetryTimer !== null) {
489
567
  clearTimeout(this.cartLookupRetryTimer);
@@ -973,6 +1051,8 @@ export class SallaConditionalOffer {
973
1051
  return;
974
1052
  this.cartRefreshPending = false;
975
1053
  this.refresh();
1054
+ // A new cart change starts a new picker retry episode: this pass asks again itself.
1055
+ this.clearPickerLookupRetry();
976
1056
  refreshCartFreeGifts().then(() => this.loadCart(), () => this.loadCart());
977
1057
  }
978
1058
  /**
@@ -1039,6 +1119,14 @@ export class SallaConditionalOffer {
1039
1119
  const text = salla.lang.get(FREE_LABEL_KEY);
1040
1120
  return text && text !== FREE_LABEL_KEY ? text : FREE_LABEL_FALLBACK;
1041
1121
  }
1122
+ getAddLabel() {
1123
+ return this.sharedLabel(ADD_LABEL_KEY, ADD_LABEL_FALLBACK);
1124
+ }
1125
+ /** A platform-wide label (not `blocks.cart_offer.*`): its own text, or `fallback` when missing. */
1126
+ sharedLabel(key, fallback) {
1127
+ const text = salla.lang.get(key);
1128
+ return text && text !== key ? text : fallback;
1129
+ }
1042
1130
  /**
1043
1131
  * Read a `blocks.cart_offer.*` translation.
1044
1132
  *
@@ -1168,6 +1256,7 @@ export class SallaConditionalOffer {
1168
1256
  // Re-asked now that `this.offer` is known, because the gift filter is keyed to
1169
1257
  // the offer id — the early pass in `load()` could only answer the COUNT.
1170
1258
  this.loadCart();
1259
+ this.loadFreeProductCards();
1171
1260
  }
1172
1261
  catch (error) {
1173
1262
  if (this.isSuperseded(generation))
@@ -1232,8 +1321,8 @@ export class SallaConditionalOffer {
1232
1321
  */
1233
1322
  loadCart() {
1234
1323
  const generation = ++this.giftsGeneration;
1235
- return Promise.all([getCartFreeGiftItems(), getCartItemCount()])
1236
- .then(([items, count]) => {
1324
+ return Promise.all([getCartFreeGiftItems(), getCartItemCount(), getCartProductLines()])
1325
+ .then(([items, count, lines]) => {
1237
1326
  if (generation !== this.giftsGeneration || this.isDisconnected)
1238
1327
  return;
1239
1328
  /**
@@ -1247,6 +1336,8 @@ export class SallaConditionalOffer {
1247
1336
  */
1248
1337
  if (this.cartRefreshPending)
1249
1338
  return;
1339
+ // Was this pass the cart, or a failed lookup's fallback? See below.
1340
+ const answered = hasAuthoritativeCartData();
1250
1341
  /**
1251
1342
  * NEVER walks a KNOWN count back to `null`. `count` is `null` whenever the
1252
1343
  * shared lookup could not answer — which, on the removal path, is every
@@ -1283,6 +1374,28 @@ export class SallaConditionalOffer {
1283
1374
  this.deferredByEmptyCart = false;
1284
1375
  this.refresh();
1285
1376
  }
1377
+ /**
1378
+ * A failed lookup hands back the cached lines, or `[]` right after a cart
1379
+ * update — not the cart. Publishing them would show an added gift as Add
1380
+ * again (one more tap is a PAID unit) and end the post-change checks below
1381
+ * as though fresh lines had arrived. Keep what we had, and while a picker
1382
+ * change is waiting on real lines — or a shown picker has never had any, so
1383
+ * Add is still disabled — ask again: the count alone schedules no retry once
1384
+ * the cart event has supplied one. A cart with no items (no banner) asks
1385
+ * nothing more.
1386
+ *
1387
+ * An offer without a picker has none of this at stake and goes on as it always
1388
+ * did: its gift card must still follow a removed gift, and nothing would retry.
1389
+ */
1390
+ const hasPicker = this.getPickerStage(this.getStages()) !== null;
1391
+ const awaitsPickerChange = this.pendingFreeProductSettledAt !== null || this.cartPageCheckAfter !== null;
1392
+ if (!answered && (hasPicker || awaitsPickerChange)) {
1393
+ if (awaitsPickerChange || (!this.cartLinesLoaded && this.hasCartItems())) {
1394
+ this.retryPickerCartLookup();
1395
+ }
1396
+ return;
1397
+ }
1398
+ this.clearPickerLookupRetry();
1286
1399
  /**
1287
1400
  * The gift card is keyed to the offer, so it can only be filled once the
1288
1401
  * offer is known. The early pass fired from `load()` runs BEFORE
@@ -1292,6 +1405,16 @@ export class SallaConditionalOffer {
1292
1405
  * one.
1293
1406
  */
1294
1407
  this.gifts = this.offer ? this.selectOwnGifts(items) : [];
1408
+ this.cartLines = this.offer ? lines : [];
1409
+ this.cartLinesLoaded = !!this.offer;
1410
+ // These lines were read after the pending change landed: the picker may act again.
1411
+ if (this.pendingFreeProductSettledAt !== null && generation > this.pendingFreeProductSettledAt) {
1412
+ this.unlockFreeProduct();
1413
+ }
1414
+ if (this.cartPageCheckAfter !== null && generation > this.cartPageCheckAfter) {
1415
+ this.cartPageCheckAfter = null;
1416
+ this.showMissingCartLines(lines);
1417
+ }
1295
1418
  })
1296
1419
  .catch(error => {
1297
1420
  this.safeLog('cart lookup failed', error);
@@ -1323,6 +1446,27 @@ export class SallaConditionalOffer {
1323
1446
  this.loadCart();
1324
1447
  }, CART_LOOKUP_RETRY_DELAYS_MS[this.cartLookupRetries]);
1325
1448
  }
1449
+ /**
1450
+ * Ask for the cart again after a lookup that could not answer while a picker change is
1451
+ * waiting on real lines. Its own bounded episode, on the count retry's delays: that one
1452
+ * ends as soon as the cart event has supplied a count, which is exactly when this is needed.
1453
+ */
1454
+ retryPickerCartLookup() {
1455
+ if (this.pickerLookupTimer !== null || this.pickerLookupRetries >= CART_LOOKUP_RETRY_DELAYS_MS.length)
1456
+ return;
1457
+ this.pickerLookupTimer = setTimeout(() => {
1458
+ this.pickerLookupTimer = null;
1459
+ this.pickerLookupRetries++;
1460
+ this.loadCart();
1461
+ }, CART_LOOKUP_RETRY_DELAYS_MS[this.pickerLookupRetries]);
1462
+ }
1463
+ clearPickerLookupRetry() {
1464
+ this.pickerLookupRetries = 0;
1465
+ if (this.pickerLookupTimer === null)
1466
+ return;
1467
+ clearTimeout(this.pickerLookupTimer);
1468
+ this.pickerLookupTimer = null;
1469
+ }
1326
1470
  /**
1327
1471
  * End the retry episode: a lookup answered, or the cart changed under it. The count
1328
1472
  * goes back to zero so a LATER failure gets its own full set of attempts.
@@ -1379,7 +1523,7 @@ export class SallaConditionalOffer {
1379
1523
  * an older backend — is measured by the offer-level value exactly as before.
1380
1524
  */
1381
1525
  getProgress(stages) {
1382
- return computeOfferStageProgress(this.offer?.details?.current_value, stages.map(({ min_spend }) => min_spend), this.offer?.details?.start_value, stages.map(({ is_applied }) => is_applied), stages.map(({ current_value }) => current_value));
1526
+ return computeOfferStageProgress(this.offer?.details?.current_value, stages.map(({ min_spend }) => min_spend), this.offer?.details?.start_value, stages.map(({ is_applied }) => is_applied), stages.map(({ current_value }) => current_value), stages.map(({ can_add_free }) => can_add_free));
1383
1527
  }
1384
1528
  /**
1385
1529
  * A tier's render key. `id` is the stable identity across refetches; the array
@@ -1436,6 +1580,15 @@ export class SallaConditionalOffer {
1436
1580
  * why every caller has a reward-less sentence to fall back to.
1437
1581
  */
1438
1582
  getRewardText(discount) {
1583
+ // CP-2171 — Figma: "Add 140 and get 2 free products". The server's label has no count.
1584
+ if (this.isPickerTier(discount)) {
1585
+ const limit = Math.max(1, Math.floor(Number(discount.max_free_products) || 1));
1586
+ if (limit === 1)
1587
+ return this.trans('free_product_reward_one');
1588
+ if (limit === 2)
1589
+ return this.trans('free_products_reward_two');
1590
+ return this.trans('free_products_reward', { count: `${limit}` });
1591
+ }
1439
1592
  return (discount?.reward_label ?? '').trim();
1440
1593
  }
1441
1594
  /**
@@ -1457,7 +1610,13 @@ export class SallaConditionalOffer {
1457
1610
  getGuidanceMessage(stages, progress) {
1458
1611
  if (progress.nextUnmetIndex === NO_STAGE_REACHED)
1459
1612
  return '';
1460
- const remainingValue = getRemainingToStage(progress.nextUnmetIndex, progress, this.stageMeasure());
1613
+ // CP-2171 — the engine's own gap for a picker tier, when the server sends it: `current_value`
1614
+ // counts the gift units the tier would free, so it can read as past the threshold while the
1615
+ // tier is still short ("conditions unmet" with no amount).
1616
+ const engineAmountNeeded = Number(stages[progress.nextUnmetIndex]?.amount_needed);
1617
+ const remainingValue = this.isPickerTier(stages[progress.nextUnmetIndex]) && Number.isFinite(engineAmountNeeded) && engineAmountNeeded > 0
1618
+ ? engineAmountNeeded
1619
+ : getRemainingToStage(progress.nextUnmetIndex, progress, this.stageMeasure());
1461
1620
  if (remainingValue <= 0)
1462
1621
  return this.trans('guidance_conditions_unmet');
1463
1622
  const remaining = this.formatRequirement(remainingValue);
@@ -1735,26 +1894,403 @@ export class SallaConditionalOffer {
1735
1894
  }, innerHTML: this.getTierSentence(discount, index, progress) }))));
1736
1895
  }))));
1737
1896
  }
1897
+ /**
1898
+ * CP-2171 — the tier the free product picker belongs to: the first `free_product` tier that
1899
+ * carries a non-empty `free_products` list. `null` on an offer without one, which keeps the
1900
+ * read-only gift card exactly as before. An empty list (every listed product unavailable) is
1901
+ * treated as no picker: there is nothing to choose, so a "Choose N" header would be a dead end.
1902
+ */
1903
+ getPickerStage(stages) {
1904
+ return stages.find(stage => this.isPickerTier(stage)) ?? null;
1905
+ }
1906
+ isPickerTier(stage) {
1907
+ return stage?.type === DiscountType.FREE_PRODUCT && Array.isArray(stage.free_products) && stage.free_products.length > 0;
1908
+ }
1909
+ /**
1910
+ * CP-5033 — the furthest tier the cart's value reaches is a gift tier with none of its gifts
1911
+ * addable (the server lists only what can be added: out of stock, hidden and deleted are left
1912
+ * out), and the cart holds none of this offer's gifts. The engine pays nothing for that tier,
1913
+ * so neither "congrats" nor "conditions unmet" is true.
1914
+ */
1915
+ areGiftsUnavailable(stages, progress) {
1916
+ const stage = stages[progress.lastReachedIndex];
1917
+ return stage?.type === DiscountType.FREE_PRODUCT
1918
+ && Array.isArray(stage.free_products) && stage.free_products.length === 0
1919
+ && this.gifts.length === 0;
1920
+ }
1921
+ /**
1922
+ * Load the picker's product cards (name, image, price) — one `salla.product.fetch` for
1923
+ * the ids not loaded yet. Never rejects: a failed lookup leaves the cards out, not the banner.
1924
+ */
1925
+ loadFreeProductCards() {
1926
+ const stage = this.getPickerStage(this.getStages());
1927
+ const ids = (stage?.free_products ?? [])
1928
+ .map(ref => String(ref.id))
1929
+ .filter(id => id && !this.freeProductCards[id]);
1930
+ if (!ids.length)
1931
+ return Promise.resolve();
1932
+ const listedIds = new Set((stage.free_products ?? []).map(ref => String(ref.id)));
1933
+ return Promise.resolve()
1934
+ .then(() => salla.product.fetch({ source: 'selected', source_value: ids }))
1935
+ .then((response) => {
1936
+ if (this.isDisconnected)
1937
+ return;
1938
+ const cards = { ...this.freeProductCards };
1939
+ (Array.isArray(response?.data) ? response.data : []).forEach((product) => {
1940
+ const id = String(product?.id ?? '');
1941
+ if (!id || !listedIds.has(id))
1942
+ return;
1943
+ cards[id] = {
1944
+ id,
1945
+ name: String(product.name ?? ''),
1946
+ imageUrl: String(product.image?.url ?? ''),
1947
+ imageAlt: String(product.image?.alt ?? product.name ?? ''),
1948
+ price: Number.isFinite(Number(product.price)) ? Number(product.price) : null,
1949
+ };
1950
+ });
1951
+ this.freeProductCards = cards;
1952
+ })
1953
+ .catch(error => this.safeLog('free product lookup failed', error));
1954
+ }
1955
+ /**
1956
+ * The picker's view of the cart for this offer.
1957
+ *
1958
+ * - `freeCount`: products a line of THIS offer made free — the engine's own verdict, read
1959
+ * off the cart lines (`added_by_offer` + `offer_id`), never re-derived here. Gifts of other
1960
+ * offers (another cart offer, Buy X Get Y) are not counted against this offer's limit.
1961
+ * - `reached`: the tier's threshold is met. Read from `current_value` against `min_spend`,
1962
+ * NOT from `is_applied`: a free product tier is only "applied" once a listed product is
1963
+ * in the cart, so gating "Add" on it would never let the customer add the first one.
1964
+ */
1965
+ getPickerState(stage) {
1966
+ const allFreeLines = this.cartLines.filter(line => line.freeQuantity > 0);
1967
+ const freeLines = this.selectOwnFreeLines(allFreeLines);
1968
+ const free = new Set(freeLines.map(line => line.productId).filter(id => id !== null));
1969
+ const limit = Math.max(1, Math.floor(Number(stage.max_free_products) || 1));
1970
+ // The engine's answer when the server sends it; `current_value >= min_spend` is only a
1971
+ // fallback — it is gross of earlier discounts, so it can say "choose" while the engine
1972
+ // would still charge for the product.
1973
+ const value = Number(stage.current_value ?? this.offer?.details?.current_value ?? 0);
1974
+ const reached = typeof stage.can_add_free === 'boolean'
1975
+ ? stage.can_add_free
1976
+ : Number.isFinite(value) && value >= Number(stage.min_spend);
1977
+ return { limit, reached, freeCount: free.size };
1978
+ }
1979
+ /**
1980
+ * The free lines this offer granted — the SAME rule as `selectOwnGifts`, so the picker and
1981
+ * the gifts card never disagree: lines matching the offer id; only when none match, the
1982
+ * lines whose `offer_id` is unreadable.
1983
+ */
1984
+ selectOwnFreeLines(lines) {
1985
+ const offerId = Number(this.offer?.id);
1986
+ if (!Number.isFinite(offerId))
1987
+ return lines;
1988
+ const owned = lines.filter(line => line.offerId !== null && Number(line.offerId) === offerId);
1989
+ return owned.length ? owned : lines.filter(line => line.offerId === null);
1990
+ }
1991
+ /**
1992
+ * Add a free product to the cart — always from the cart page, never a redirect.
1993
+ *
1994
+ * Quick-add, exactly like the single free product was auto-added before CP-2171: a variant
1995
+ * (SKU) product gets its default SKU — `is_default` first, or the first in stock when stock
1996
+ * is per SKU (`AddToCart::addItemToCart()`) — with that SKU's option values. Any listed
1997
+ * product in the cart counts, so the engine then frees it. A failed add is left to the SDK,
1998
+ * which already shows the shopper its error notice.
1999
+ */
2000
+ addFreeProduct(card) {
2001
+ if (this.pendingFreeProduct)
2002
+ return;
2003
+ this.pendingFreeProduct = card.id;
2004
+ this.runFreeProductChange(() => Promise.resolve(salla.cart.addItem(card.id)).then(() => { this.cartPageCheckAfter = this.giftsGeneration; }), 'free product add failed');
2005
+ }
2006
+ /** Its own method so specs can observe it; the mock window cannot reload. */
2007
+ reloadPage() {
2008
+ window.location.reload();
2009
+ }
2010
+ /**
2011
+ * Its own method so specs can stub it; the mock window has no `fetch`. Aborted after
2012
+ * `CART_PAGE_FETCH_TIMEOUT_MS`: a fetch that never settles would hold `insertingCartLines`
2013
+ * for the page view, and every later line would only be queued behind it.
2014
+ */
2015
+ fetchCartPageHtml() {
2016
+ const controller = new AbortController();
2017
+ const timer = setTimeout(() => controller.abort(), CART_PAGE_FETCH_TIMEOUT_MS);
2018
+ // `no-store`: a cached copy of the page would lack the new line and force the reload.
2019
+ return fetch(window.location.href, { cache: 'no-store', credentials: 'same-origin', headers: { Accept: 'text/html' }, signal: controller.signal })
2020
+ .then(response => (response.ok ? response.text() : Promise.reject(new Error(`cart page ${response.status}`))))
2021
+ .finally(() => clearTimeout(timer));
2022
+ }
2023
+ /**
2024
+ * Show the cart lines an add from the picker created that the page has no `form#item-<id>`
2025
+ * for. Themes render cart lines on the server and only refresh lines already on the page, so
2026
+ * the new line (and its price) would otherwise appear only after a manual refresh.
2027
+ *
2028
+ * The cart page is fetched again and the missing line forms are inserted, in the server's
2029
+ * order — no reload. The fetched markup is fresh, so its prices need no update. Its inline
2030
+ * handlers and web components work as on a loaded page; only what the theme's own script wires
2031
+ * to a line at page load (e.g. raed's loading overlay on a quantity change) is missing.
2032
+ * Any failure (fetch, a line the fetched page lacks) falls back to `reloadForMissingCartLines`.
2033
+ *
2034
+ * Left alone on a page with no cart line forms at all (not a cart page this can reason about).
2035
+ * One fetch at a time; lines read meanwhile are checked again after it.
2036
+ */
2037
+ showMissingCartLines(lines) {
2038
+ if (this.insertingCartLines) {
2039
+ this.pendingCartPageLines = lines;
2040
+ return;
2041
+ }
2042
+ const missing = this.missingCartLineIds(lines);
2043
+ if (!missing.length)
2044
+ return;
2045
+ this.insertingCartLines = true;
2046
+ // Started inside the chain, so a synchronous throw (no `fetch`) still reaches `catch` and `finally`.
2047
+ Promise.resolve()
2048
+ .then(() => this.fetchCartPageHtml())
2049
+ .then(html => {
2050
+ if (this.isDisconnected || this.cartChangedDuringPageFetch())
2051
+ return;
2052
+ const stillMissing = this.missingCartLineIds(this.cartLines);
2053
+ if (stillMissing.length && !this.insertCartLineForms(html, stillMissing)) {
2054
+ this.reloadForMissingCartLines(stillMissing);
2055
+ }
2056
+ })
2057
+ .catch(error => {
2058
+ this.safeLog('cart page fetch for the new lines failed, reloading', error);
2059
+ if (this.isDisconnected || this.cartChangedDuringPageFetch())
2060
+ return;
2061
+ this.reloadForMissingCartLines(this.missingCartLineIds(this.cartLines));
2062
+ })
2063
+ .finally(() => {
2064
+ this.insertingCartLines = false;
2065
+ const pending = this.pendingCartPageLines;
2066
+ this.pendingCartPageLines = null;
2067
+ if (pending && !this.isDisconnected)
2068
+ this.showMissingCartLines(pending);
2069
+ });
2070
+ }
2071
+ /**
2072
+ * A picker change started while the cart page was fetched (e.g. a second add),
2073
+ * so neither the fetched page nor the cart lines can be trusted: inserting now could bring
2074
+ * back a line the cart no longer has. The answer is dropped and the cart read after that
2075
+ * change checks the page again (the change itself arms `cartPageCheckAfter` when it lands;
2076
+ * armed here too, for a change that fails and so arms nothing).
2077
+ *
2078
+ * Lines are always read from `cartLines` once the page arrives, never from the call's own
2079
+ * `lines`: a change that landed during the fetch has already replaced them there.
2080
+ */
2081
+ cartChangedDuringPageFetch() {
2082
+ // Lines not read since a change (the lock timed out on them) are no basis either.
2083
+ if (this.pendingFreeProduct === null && this.cartLinesLoaded)
2084
+ return false;
2085
+ if (this.cartPageCheckAfter === null)
2086
+ this.cartPageCheckAfter = this.giftsGeneration;
2087
+ return true;
2088
+ }
2089
+ /** Ids of `lines` the page has no form for, sorted; empty on a page with no cart line forms. */
2090
+ missingCartLineIds(lines) {
2091
+ try {
2092
+ if (!document.querySelector(CART_LINE_FORMS))
2093
+ return [];
2094
+ return lines.map(line => line.id).filter(id => !document.getElementById(`item-${id}`)).sort();
2095
+ }
2096
+ catch (error) {
2097
+ this.safeLog('cart page check after a free product add failed', error);
2098
+ return [];
2099
+ }
2100
+ }
2101
+ /**
2102
+ * Insert the `missing` line forms from the fetched cart page `html`, each right before the next
2103
+ * line the page already has (in the fetched order), else after the last one. Returns false —
2104
+ * nothing inserted — when the fetched page lacks any of them.
2105
+ */
2106
+ insertCartLineForms(html, missing) {
2107
+ try {
2108
+ const fetched = Array.from(new DOMParser().parseFromString(html, 'text/html').querySelectorAll(CART_LINE_FORMS));
2109
+ // `getAttribute`, not `form.id`: a line form holds an `<input name="id">`, which shadows the property.
2110
+ const fetchedIds = fetched.map(form => form.getAttribute('id'));
2111
+ if (missing.some(id => !fetchedIds.includes(`item-${id}`)))
2112
+ return false;
2113
+ fetched.forEach((form, index) => {
2114
+ const id = fetchedIds[index];
2115
+ if (!missing.includes(id.slice('item-'.length)) || document.getElementById(id))
2116
+ return;
2117
+ // Moved, not cloned: inserting adopts it into this document, and its web components
2118
+ // upgrade once connected. Its inline handlers (`onchange`, delete `onclick`) come along.
2119
+ const next = fetchedIds.slice(index + 1).map(nextId => document.getElementById(nextId)).find(Boolean);
2120
+ if (next) {
2121
+ next.parentNode.insertBefore(form, next);
2122
+ }
2123
+ else {
2124
+ const onPage = document.querySelectorAll(CART_LINE_FORMS);
2125
+ const last = onPage[onPage.length - 1];
2126
+ last.parentNode.insertBefore(form, last.nextSibling);
2127
+ }
2128
+ });
2129
+ return missing.every(id => document.getElementById(`item-${id}`));
2130
+ }
2131
+ catch (error) {
2132
+ this.safeLog('cart line insert failed', error);
2133
+ return false;
2134
+ }
2135
+ }
2136
+ /**
2137
+ * Reload once, when the missing lines could not be inserted. Never twice for the same missing
2138
+ * lines, so a line a theme never renders cannot loop the page. The guard lives in
2139
+ * `sessionStorage`; where storage is blocked the page still reloads, but never twice in a row.
2140
+ */
2141
+ reloadForMissingCartLines(missing) {
2142
+ if (!missing.length)
2143
+ return;
2144
+ const signature = missing.join(',');
2145
+ const stored = this.readReloadGuard();
2146
+ if (stored === signature)
2147
+ return;
2148
+ if (stored === undefined && this.pageWasReloaded())
2149
+ return;
2150
+ this.writeReloadGuard(signature);
2151
+ this.reloadPage();
2152
+ }
2153
+ /** The reload guard, or `undefined` when storage cannot be read (blocked, disabled). */
2154
+ readReloadGuard() {
2155
+ try {
2156
+ return window.sessionStorage.getItem(CART_PAGE_RELOAD_KEY);
2157
+ }
2158
+ catch {
2159
+ return undefined;
2160
+ }
2161
+ }
2162
+ writeReloadGuard(signature) {
2163
+ try {
2164
+ window.sessionStorage.setItem(CART_PAGE_RELOAD_KEY, signature);
2165
+ }
2166
+ catch {
2167
+ // Storage full or blocked: pageWasReloaded() is the guard on the next load.
2168
+ }
2169
+ }
2170
+ /** Whether this page load is itself a reload — the guard when storage is unavailable. */
2171
+ pageWasReloaded() {
2172
+ try {
2173
+ const navigation = window.performance?.getEntriesByType?.('navigation')?.[0];
2174
+ return navigation?.type === 'reload';
2175
+ }
2176
+ catch {
2177
+ return false;
2178
+ }
2179
+ }
2180
+ /**
2181
+ * Run one add. A failure unlocks at once — nothing changed. A success stays locked
2182
+ * until a cart load that started after it publishes (see `loadCart`), or the fallback fires.
2183
+ */
2184
+ runFreeProductChange(change, failure) {
2185
+ Promise.resolve()
2186
+ .then(change)
2187
+ .then(() => {
2188
+ if (this.isDisconnected)
2189
+ return this.unlockFreeProduct();
2190
+ this.pendingFreeProductSettledAt = this.giftsGeneration;
2191
+ this.pendingFreeProductTimer = setTimeout(() => this.unlockOnStaleLines(), FREE_PRODUCT_UNLOCK_FALLBACK_MS);
2192
+ })
2193
+ .catch(error => {
2194
+ this.safeLog(failure, error);
2195
+ this.unlockFreeProduct();
2196
+ // A failed change fires no cart event, so nothing else checks a cart page answer
2197
+ // that was dropped while this change was in flight.
2198
+ if (this.cartPageCheckAfter !== null)
2199
+ this.loadCart();
2200
+ });
2201
+ }
2202
+ /**
2203
+ * The lock's fallback: no cart lines read after the change have arrived, so the lines on
2204
+ * screen are the ones from BEFORE it. Release the lock, mark the lines unread and ask again
2205
+ * on a fresh read — `loadCart` retries within its bound while the picker has no lines.
2206
+ */
2207
+ unlockOnStaleLines() {
2208
+ this.unlockFreeProduct();
2209
+ this.cartLinesLoaded = false;
2210
+ this.clearPickerLookupRetry();
2211
+ refreshCartFreeGifts().then(() => this.loadCart(), () => this.loadCart());
2212
+ }
2213
+ unlockFreeProduct() {
2214
+ if (this.pendingFreeProductTimer !== null)
2215
+ clearTimeout(this.pendingFreeProductTimer);
2216
+ this.pendingFreeProductTimer = null;
2217
+ this.pendingFreeProductSettledAt = null;
2218
+ this.pendingFreeProduct = null;
2219
+ }
2220
+ /**
2221
+ * CP-2171 — Figma's banner line at the gift tier: "You got N free products", with
2222
+ * "Choose them now" while none is picked yet. N is the tier's limit, not the count picked.
2223
+ */
2224
+ pickerGotMessage(limit, awaitsPick) {
2225
+ if (limit === 1)
2226
+ return this.trans(awaitsPick ? 'free_product_got_pick_one' : 'free_product_got_one');
2227
+ if (limit === 2)
2228
+ return this.trans(awaitsPick ? 'free_products_got_pick_two' : 'free_products_got_two');
2229
+ return this.trans(awaitsPick ? 'free_products_got_pick' : 'free_products_got', { count: `${limit}` });
2230
+ }
2231
+ getPickerCards(stage) {
2232
+ return (stage.free_products ?? [])
2233
+ .map(ref => this.freeProductCards[String(ref.id)])
2234
+ .filter(Boolean);
2235
+ }
2236
+ /** CP-2171 — Figma "Choose N free products": the counter, then one card per listed product. */
2237
+ renderPicker(stage) {
2238
+ const state = this.getPickerState(stage);
2239
+ const cards = this.getPickerCards(stage);
2240
+ // Cards still loading, or the lookup failed: no header and counter over an empty list.
2241
+ if (!cards.length)
2242
+ return null;
2243
+ return (h("div", { class: "s-conditional-offer-picker", "data-picker-reached": state.reached ? 'true' : 'false' }, h("div", { class: "s-conditional-offer-picker-header" }, h("span", { class: "s-conditional-offer-picker-counter", "aria-label": `${state.freeCount}/${state.limit}` }, `${state.freeCount}/${state.limit}`), h("span", { class: "s-conditional-offer-picker-title" }, state.limit === 1
2244
+ ? this.trans('free_product_choose_one')
2245
+ : state.limit === 2
2246
+ ? this.trans('free_products_choose_two')
2247
+ : this.trans('free_products_choose', { count: `${state.limit}` }))), h("ul", { class: "s-conditional-offer-picker-list" }, cards.map(card => this.renderPickerCard(card)))));
2248
+ }
2249
+ /**
2250
+ * One picker card: the product, its normal price and Add — the same on every card, whether
2251
+ * the product is in the cart, free or not. Add is never disabled: every tap adds one unit at
2252
+ * the normal price (also once the product is in the cart, and after the free limit is used
2253
+ * up) and the engine decides what is free. What IS free shows in the "Free products" panel
2254
+ * below, never on the card. A tap while the previous add is still in flight is ignored
2255
+ * (`addFreeProduct`), so one double tap is one unit.
2256
+ *
2257
+ * No remove: the picker never deletes a cart line. A free line can also hold paid units, and
2258
+ * deleting it would take what the customer bought with the gift — products leave the cart
2259
+ * from their own cart line only.
2260
+ */
2261
+ renderPickerCard(card) {
2262
+ return (h("li", { class: "s-conditional-offer-picker-card", "data-product-id": card.id }, h("span", { class: "s-conditional-offer-picker-thumb" }, card.imageUrl ? (h("img", { class: "s-conditional-offer-picker-image", src: card.imageUrl, alt: card.imageAlt || card.name, loading: "lazy", decoding: "async" })) : (h("i", { class: "sicon-gift", "aria-hidden": "true" }))), h("span", { class: "s-conditional-offer-picker-name" }, card.name), card.price !== null ? (h("span", { class: "s-conditional-offer-picker-prices" }, h("span", { class: "s-conditional-offer-picker-price-regular", innerHTML: salla.money(card.price) }))) : null, h("span", { class: "s-conditional-offer-picker-actions" }, h("button", { type: "button", class: "s-conditional-offer-picker-add", onClick: () => this.addFreeProduct(card) }, this.getAddLabel()))));
2263
+ }
1738
2264
  /**
1739
2265
  * "ادارة العرض". Rendered TWICE with complementary responsive visibility rather than
1740
2266
  * moved with `order:`: desktop wants it inline with the banner row, mobile wants a
1741
2267
  * full-width bar BELOW the gifts card, and those are different places
1742
2268
  * in the flow, not a different order within one flex line.
1743
2269
  */
1744
- renderGiftsToggle(variant) {
2270
+ renderGiftsToggle(variant, hasPicker) {
1745
2271
  return (h("button", { type: "button", id: this.elementId(`toggle-${variant}`), class: {
1746
2272
  's-conditional-offer-toggle': true,
1747
2273
  's-conditional-offer-toggle-inline': variant === 'inline',
1748
2274
  's-conditional-offer-toggle-bar': variant === 'bar',
1749
- }, "aria-expanded": this.isGiftsOpen ? 'true' : 'false', "aria-controls": this.elementId('gifts'), onClick: () => (this.isGiftsOpen = !this.isGiftsOpen) }, h("span", null, this.trans('manage_offer')), h("i", { class: {
2275
+ }, "aria-expanded": this.isGiftsOpen ? 'true' : 'false', "aria-controls": this.elementId('gifts'), onClick: () => (this.isGiftsOpen = !this.isGiftsOpen) }, h("span", null, this.trans(hasPicker ? 'offer_toggle' : 'manage_offer')), h("i", { class: {
1750
2276
  'sicon-keyboard_arrow_down': true,
1751
2277
  's-conditional-offer-toggle-icon': true,
1752
2278
  's-conditional-offer-toggle-icon-open': this.isGiftsOpen,
1753
2279
  }, "aria-hidden": "true" })));
1754
2280
  }
1755
- /** Spec §5 — the gifts card: one read-only row per gift line. */
1756
- renderGiftsCard() {
1757
- return (h("div", { class: "s-conditional-offer-panel", id: this.elementId('gifts') }, h("div", { class: "s-conditional-offer-gifts-header" }, h("span", { class: "s-conditional-offer-gifts-icon" }, h("i", { class: "sicon-gift", "aria-hidden": "true" })), h("span", { class: "s-conditional-offer-gifts-title" }, this.trans('free_products_title'))), h("ul", { class: "s-conditional-offer-gifts-list" }, this.gifts.map(gift => this.renderGiftRow(gift)))));
2281
+ /**
2282
+ * Spec §5 — the gifts card: one row per gift line. Read-only on an offer without a picker,
2283
+ * exactly as before; with the CP-2171 picker it becomes the Figma "Free products" panel,
2284
+ * shown only once a gift is in the cart. Its rows are read-only, as are the picker cards: a
2285
+ * gift leaves the cart from its own cart line.
2286
+ */
2287
+ renderGiftsCard(pickerStage = null) {
2288
+ // No picker: the card exactly as before CP-2171 — no wrapper, so no new box styling.
2289
+ if (!pickerStage) {
2290
+ return (h("div", { class: "s-conditional-offer-panel", id: this.elementId('gifts') }, h("div", { class: "s-conditional-offer-gifts-header" }, h("span", { class: "s-conditional-offer-gifts-icon" }, h("i", { class: "sicon-gift", "aria-hidden": "true" })), h("span", { class: "s-conditional-offer-gifts-title" }, this.trans('free_products_title'))), h("ul", { class: "s-conditional-offer-gifts-list" }, this.gifts.map(gift => this.renderGiftRow(gift)))));
2291
+ }
2292
+ const state = this.getPickerState(pickerStage);
2293
+ return (h("div", { class: "s-conditional-offer-panel", id: this.elementId('gifts') }, state.reached ? this.renderPicker(pickerStage) : null, this.gifts.length ? (h("div", { class: "s-conditional-offer-gifts" }, h("div", { class: "s-conditional-offer-gifts-header" }, h("span", { class: "s-conditional-offer-gifts-icon" }, h("i", { class: "sicon-gift", "aria-hidden": "true" })), h("span", { class: "s-conditional-offer-gifts-title" }, this.sharedLabel(FREE_PRODUCTS_TITLE_KEY, FREE_PRODUCTS_TITLE_FALLBACK))), h("ul", { class: "s-conditional-offer-gifts-list" }, this.gifts.map(gift => this.renderGiftRow(gift))))) : null));
1758
2294
  }
1759
2295
  renderGiftRow(gift) {
1760
2296
  return (h("li", { class: "s-conditional-offer-gift", "data-item-id": gift.id }, h("span", { class: "s-conditional-offer-gift-thumb" }, gift.imageUrl ? (h("img", { class: "s-conditional-offer-gift-image", src: gift.imageUrl, alt: gift.imageAlt || gift.name, loading: "lazy", decoding: "async" })) : (h("i", { class: "sicon-gift", "aria-hidden": "true" }))), h("div", { class: "s-conditional-offer-gift-body" }, h("span", { class: "s-conditional-offer-gift-name" }, gift.name || this.trans('free_gift')), h("span", { class: "s-conditional-offer-gift-prices" }, h("span", { class: "s-conditional-offer-gift-free" }, this.getFreeLabel()), gift.originalPrice !== null ? (
@@ -1801,12 +2337,26 @@ export class SallaConditionalOffer {
1801
2337
  const progress = this.getProgress(stages);
1802
2338
  const guidance = this.getGuidanceMessage(stages, progress);
1803
2339
  const detailsTitle = this.trans('details_title');
1804
- // Spec §2 — the accordion exists only when there is something to manage.
1805
- const hasGifts = this.gifts.length > 0;
1806
- return (h(Host, { class: "s-conditional-offer-root" }, h("div", { class: "s-conditional-offer-banner" }, h("div", { class: "s-conditional-offer-lead" }, this.renderBadges(stages, progress), h("div", { class: "s-conditional-offer-text" }, h("div", { class: "s-conditional-offer-heading", onMouseEnter: () => (this.isTooltipHovered = true), onMouseLeave: () => (this.isTooltipHovered = false) }, h("span", { class: "s-conditional-offer-name" }, this.offer.title), h("span", { class: "s-conditional-offer-info", ref: (el) => (this.infoAnchorEl = el) }, h("button", { type: "button", class: "s-conditional-offer-info-trigger", ref: (el) => (this.infoTriggerEl = el), "aria-label": detailsTitle, title: detailsTitle, "aria-expanded": this.isTooltipOpen ? 'true' : 'false', "aria-describedby": this.elementId('tooltip'), onClick: () => this.toggleTooltip() }, h(InformationIcon, { class: "s-conditional-offer-info-icon" })), this.isTooltipOpen ? this.renderTooltip(stages, progress) : null)), progress.allMet ? (h("div", { class: "s-conditional-offer-message", role: "status" }, this.trans('congrats'))) : guidance ? (
2340
+ // Spec §2 — the accordion exists only when there is something to manage: gift lines,
2341
+ // or (CP-2171) a free product picker — shown only once the customer has reached its
2342
+ // stage, then before anything is picked too.
2343
+ const pickerStage = this.getPickerStage(stages);
2344
+ const pickerState = pickerStage !== null ? this.getPickerState(pickerStage) : null;
2345
+ const isPickerReached = pickerState?.reached ?? false;
2346
+ const hasGifts = this.gifts.length > 0 || (isPickerReached && this.getPickerCards(pickerStage).length > 0);
2347
+ // The gift tier is reached but none of this offer's gifts is in the cart: the engine
2348
+ // grants that tier only once one is, and pays nothing until then (no fallback to a lower
2349
+ // tier). So neither "congrats" nor "conditions unmet" is true — ask for the pick. Only
2350
+ // once the cart lines are read, or a cart that holds the gift would flash this line.
2351
+ const awaitsGiftPick = isPickerReached && this.cartLinesLoaded && pickerState.freeCount === 0;
2352
+ // "You got N free products" replaces congrats only when the gift tier is the offer's last:
2353
+ // below a higher tier, `guidance` (or congrats for that higher tier) is the truer line.
2354
+ const gotGifts = progress.allMet && isPickerReached && pickerState.freeCount > 0
2355
+ && stages[stages.length - 1] === pickerStage;
2356
+ return (h(Host, { class: "s-conditional-offer-root" }, h("div", { class: "s-conditional-offer-banner" }, h("div", { class: "s-conditional-offer-lead" }, this.renderBadges(stages, progress), h("div", { class: "s-conditional-offer-text" }, h("div", { class: "s-conditional-offer-heading", onMouseEnter: () => (this.isTooltipHovered = true), onMouseLeave: () => (this.isTooltipHovered = false) }, h("span", { class: "s-conditional-offer-name" }, this.offer.title), h("span", { class: "s-conditional-offer-info", ref: (el) => (this.infoAnchorEl = el) }, h("button", { type: "button", class: "s-conditional-offer-info-trigger", ref: (el) => (this.infoTriggerEl = el), "aria-label": detailsTitle, title: detailsTitle, "aria-expanded": this.isTooltipOpen ? 'true' : 'false', "aria-describedby": this.elementId('tooltip'), onClick: () => this.toggleTooltip() }, h(InformationIcon, { class: "s-conditional-offer-info-icon" })), this.isTooltipOpen ? this.renderTooltip(stages, progress) : null)), awaitsGiftPick || gotGifts ? (h("div", { class: "s-conditional-offer-message", role: "status" }, this.pickerGotMessage(pickerState.limit, awaitsGiftPick))) : this.areGiftsUnavailable(stages, progress) ? (h("div", { class: "s-conditional-offer-message", role: "status" }, this.trans('free_products_unavailable'))) : progress.allMet ? (h("div", { class: "s-conditional-offer-message", role: "status" }, this.trans('congrats'))) : guidance ? (
1807
2357
  // innerHTML: the `:remaining` half carries `salla.money()` markup;
1808
2358
  // the reward half is escaped in `getGuidanceMessage`.
1809
- h("div", { class: "s-conditional-offer-message", innerHTML: guidance })) : null)), hasGifts ? this.renderGiftsToggle('inline') : null), hasGifts && this.isGiftsOpen ? this.renderGiftsCard() : null, hasGifts ? this.renderGiftsToggle('bar') : null));
2359
+ h("div", { class: "s-conditional-offer-message", innerHTML: guidance })) : null)), hasGifts ? this.renderGiftsToggle('inline', !!pickerStage) : null), hasGifts && this.isGiftsOpen ? this.renderGiftsCard(pickerStage) : null, hasGifts ? this.renderGiftsToggle('bar', !!pickerStage) : null));
1810
2360
  }
1811
2361
  static get is() { return "salla-conditional-offer"; }
1812
2362
  static get originalStyleUrls() {
@@ -1825,6 +2375,10 @@ export class SallaConditionalOffer {
1825
2375
  "isLoading": {},
1826
2376
  "canRender": {},
1827
2377
  "gifts": {},
2378
+ "cartLines": {},
2379
+ "cartLinesLoaded": {},
2380
+ "freeProductCards": {},
2381
+ "pendingFreeProduct": {},
1828
2382
  "cartItemCount": {},
1829
2383
  "isTooltipHovered": {},
1830
2384
  "isTooltipPinned": {},