@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.
- package/dist/cjs/{Helper-sq6ztayx.js → Helper-BSFw7uVh.js} +1 -1
- package/dist/cjs/{filepond-BMxlEH8M.js → filepond-DzJcpvBM.js} +1 -1
- package/dist/cjs/{filepond-plugin-file-poster-DVSz4StJ.js → filepond-plugin-file-poster-BILkbyw_.js} +1 -1
- package/dist/cjs/{filepond-plugin-file-validate-size-CXJiLSzt.js → filepond-plugin-file-validate-size-DIJUAZji.js} +1 -1
- package/dist/cjs/{filepond-plugin-file-validate-type-DWK85msl.js → filepond-plugin-file-validate-type-4lpOf3hf.js} +1 -1
- package/dist/cjs/{filepond-plugin-image-edit-BHttJI5X.js → filepond-plugin-image-edit-DtE3Wj_4.js} +1 -1
- package/dist/cjs/{filepond-plugin-image-exif-orientation-DTKIO2zE.js → filepond-plugin-image-exif-orientation-Dc_jwifJ.js} +1 -1
- package/dist/cjs/{filepond-plugin-image-preview-BaRxwhFa.js → filepond-plugin-image-preview-D_8qt0dd.js} +1 -1
- package/dist/cjs/{index-BJ3-fFSF.js → index-D0d25H-r.js} +2 -2
- package/dist/cjs/{intlTelInput-DNoOb73M.js → intlTelInput-DqmuleM6.js} +1 -1
- package/dist/cjs/loader.cjs.js +2 -2
- package/dist/cjs/salla-accordion-body_2.cjs.entry.js +1 -1
- package/dist/cjs/salla-accordion_6.cjs.entry.js +2 -2
- package/dist/cjs/salla-add-product-button_5.cjs.entry.js +2 -2
- package/dist/cjs/salla-advertisement.cjs.entry.js +1 -1
- package/dist/cjs/salla-alert_2.cjs.entry.js +1 -1
- package/dist/cjs/salla-app-install-alert.cjs.entry.js +1 -1
- package/dist/cjs/salla-applied-filters.cjs.entry.js +2 -2
- package/dist/cjs/salla-apps-icons.cjs.entry.js +1 -1
- package/dist/cjs/salla-attachment-view_9.cjs.entry.js +11 -11
- package/dist/cjs/salla-badge.cjs.entry.js +1 -1
- package/dist/cjs/salla-basket-gap.cjs.entry.js +1 -1
- package/dist/cjs/salla-bullet-delivery_3.cjs.entry.js +1 -1
- package/dist/cjs/salla-cart-coupons.cjs.entry.js +1 -1
- package/dist/cjs/salla-cart-item-offers_2.cjs.entry.js +5 -1
- package/dist/cjs/salla-cart-summary-card.cjs.entry.js +1 -1
- package/dist/cjs/salla-cashback-banner.cjs.entry.js +1 -1
- package/dist/cjs/salla-comment-factors.cjs.entry.js +1 -1
- package/dist/cjs/salla-comment-form_9.cjs.entry.js +2 -2
- package/dist/cjs/salla-conditional-offer.cjs.entry.js +628 -19
- package/dist/cjs/salla-contacts.cjs.entry.js +1 -1
- package/dist/cjs/salla-cookies-bar.cjs.entry.js +1 -1
- package/dist/cjs/salla-count-down.cjs.entry.js +1 -1
- package/dist/cjs/salla-custom-fields.cjs.entry.js +1 -1
- package/dist/cjs/salla-delivery-promise.cjs.entry.js +1 -1
- package/dist/cjs/salla-edit-order-button.cjs.entry.js +1 -1
- package/dist/cjs/salla-filters-widget.cjs.entry.js +2 -2
- package/dist/cjs/salla-filters.cjs.entry.js +2 -2
- package/dist/cjs/salla-free-shipping-banner.cjs.entry.js +1 -1
- package/dist/cjs/salla-free-shipping-tag.cjs.entry.js +1 -1
- package/dist/cjs/salla-fulfillment-methods.cjs.entry.js +1 -1
- package/dist/cjs/salla-gift-card-redeem_2.cjs.entry.js +1 -1
- package/dist/cjs/salla-gifting.cjs.entry.js +1 -1
- package/dist/cjs/salla-hook.cjs.entry.js +1 -1
- package/dist/cjs/salla-infinite-scroll.cjs.entry.js +2 -2
- package/dist/cjs/salla-installment.cjs.entry.js +1 -1
- package/dist/cjs/salla-list-tile.cjs.entry.js +1 -1
- package/dist/cjs/salla-localization-modal.cjs.entry.js +1 -1
- package/dist/cjs/salla-login-modal.cjs.entry.js +1 -1
- package/dist/cjs/salla-loyalty-banner_10.cjs.entry.js +1 -1
- package/dist/cjs/salla-loyalty-exchange-product-card.cjs.entry.js +1 -1
- package/dist/cjs/salla-loyalty-points-banner.cjs.entry.js +1 -1
- package/dist/cjs/salla-loyalty-points-history.cjs.entry.js +1 -1
- package/dist/cjs/salla-loyalty-prize-item.cjs.entry.js +1 -1
- package/dist/cjs/salla-loyalty.cjs.entry.js +1 -1
- package/dist/cjs/salla-maintenance-alert.cjs.entry.js +1 -1
- package/dist/cjs/salla-map_3.cjs.entry.js +1 -1
- package/dist/cjs/salla-menu.cjs.entry.js +1 -1
- package/dist/cjs/salla-metadata.cjs.entry.js +1 -1
- package/dist/cjs/salla-mobile-update-modal.cjs.entry.js +1 -1
- package/dist/cjs/salla-multiple-bundle-product-cart_2.cjs.entry.js +1 -1
- package/dist/cjs/salla-multiple-bundle-product-options-modal_2.cjs.entry.js +1 -1
- package/dist/cjs/salla-multiple-bundle-product.cjs.entry.js +1 -1
- package/dist/cjs/salla-next-order-coupon.cjs.entry.js +8 -1
- package/dist/cjs/salla-notification-item_2.cjs.entry.js +2 -2
- package/dist/cjs/salla-offer-modal.cjs.entry.js +1 -1
- package/dist/cjs/salla-offer.cjs.entry.js +1 -1
- package/dist/cjs/salla-order-branch.cjs.entry.js +1 -1
- package/dist/cjs/salla-order-cancel.cjs.entry.js +1 -1
- package/dist/cjs/salla-order-details-multiple-bundle-product.cjs.entry.js +1 -1
- package/dist/cjs/salla-order-details-options.cjs.entry.js +1 -1
- package/dist/cjs/salla-order-details.cjs.entry.js +1 -1
- package/dist/cjs/salla-order-edit-item.cjs.entry.js +1 -1
- package/dist/cjs/salla-order-edit-product-card.cjs.entry.js +1 -1
- package/dist/cjs/salla-order-edit.cjs.entry.js +1 -1
- package/dist/cjs/salla-order-items.cjs.entry.js +1 -1
- package/dist/cjs/salla-order-options-modal.cjs.entry.js +1 -1
- package/dist/cjs/salla-order-payment.cjs.entry.js +1 -1
- package/dist/cjs/salla-order-rewards.cjs.entry.js +1 -1
- package/dist/cjs/salla-order-shipments.cjs.entry.js +2 -11
- package/dist/cjs/salla-order-summary.cjs.entry.js +1 -1
- package/dist/cjs/salla-order-totals-card.cjs.entry.js +1 -1
- package/dist/cjs/salla-orders.cjs.entry.js +2 -2
- package/dist/cjs/salla-payments.cjs.entry.js +1 -1
- package/dist/cjs/salla-placeholder.cjs.entry.js +1 -1
- package/dist/cjs/salla-price-range.cjs.entry.js +1 -1
- package/dist/cjs/salla-product-card-embed.cjs.entry.js +1 -1
- package/dist/cjs/salla-product-card.cjs.entry.js +1 -1
- package/dist/cjs/salla-product-size-guide.cjs.entry.js +1 -1
- package/dist/cjs/salla-products-list.cjs.entry.js +2 -2
- package/dist/cjs/salla-products-slider.cjs.entry.js +2 -2
- package/dist/cjs/salla-quick-order.cjs.entry.js +1 -1
- package/dist/cjs/salla-rating-modal-v2.cjs.entry.js +2 -2
- package/dist/cjs/salla-rating-modal.cjs.entry.js +2 -2
- package/dist/cjs/salla-review-factors-tags.cjs.entry.js +1 -1
- package/dist/cjs/salla-review-order-item.cjs.entry.js +1 -1
- package/dist/cjs/salla-scopes.cjs.entry.js +1 -1
- package/dist/cjs/salla-search-product-card.cjs.entry.js +1 -1
- package/dist/cjs/salla-search.cjs.entry.js +2 -2
- package/dist/cjs/salla-skeleton.cjs.entry.js +1 -1
- package/dist/cjs/salla-social-share.cjs.entry.js +1 -1
- package/dist/cjs/salla-social.cjs.entry.js +1 -1
- package/dist/cjs/salla-tab-content_3.cjs.entry.js +1 -1
- package/dist/cjs/salla-thank-you-header.cjs.entry.js +1 -1
- package/dist/cjs/salla-thank-you-page.cjs.entry.js +1 -1
- package/dist/cjs/salla-tiered-offer.cjs.entry.js +1 -1
- package/dist/cjs/salla-trust-badges.cjs.entry.js +1 -1
- package/dist/cjs/salla-user-menu.cjs.entry.js +1 -1
- package/dist/cjs/salla-user-profile.cjs.entry.js +1 -1
- package/dist/cjs/salla-user-settings.cjs.entry.js +1 -1
- package/dist/cjs/salla-verify.cjs.entry.js +1 -1
- package/dist/cjs/salla-wallet.cjs.entry.js +1 -1
- package/dist/cjs/salla-wishlist-actions.cjs.entry.js +1 -1
- package/dist/cjs/salla-wishlist-summary.cjs.entry.js +2 -2
- package/dist/cjs/twilight.cjs.js +2 -2
- package/dist/cjs/{vanilla-picker-CceY3g8c.js → vanilla-picker-DP9Hy8IR.js} +1 -1
- package/dist/collection/components/salla-cart-item-offers/salla-cart-item-offers.js +4 -0
- package/dist/collection/components/salla-conditional-offer/salla-conditional-offer.css +139 -0
- package/dist/collection/components/salla-conditional-offer/salla-conditional-offer.js +568 -14
- package/dist/collection/components/salla-next-order-coupon/salla-next-order-coupon.js +7 -0
- package/dist/collection/components/salla-order-shipments/salla-order-shipments.js +1 -10
- package/dist/collection/shared/cart-free-gifts.js +42 -1
- package/dist/collection/shared/offer-stages.js +22 -3
- package/dist/components/index.js +2 -2
- package/dist/components/salla-cart-item-offers.js +4 -0
- package/dist/components/salla-conditional-offer.js +631 -18
- package/dist/components/salla-next-order-coupon.js +7 -0
- package/dist/components/salla-order-shipments.js +1 -10
- package/dist/docs.json +2 -2
- package/dist/esm/{Helper-BwzPfwZJ.js → Helper-DmnUAusv.js} +1 -1
- package/dist/esm/{filepond-JNV-1zqR.js → filepond-DZbzdjkK.js} +1 -1
- package/dist/esm/{filepond-plugin-file-poster-nFqNHCag.js → filepond-plugin-file-poster-D9cjvoj-.js} +1 -1
- package/dist/esm/{filepond-plugin-file-validate-size-P3KmOTH8.js → filepond-plugin-file-validate-size-D-kjllC-.js} +1 -1
- package/dist/esm/{filepond-plugin-file-validate-type-BV0Ctr7U.js → filepond-plugin-file-validate-type-D9hsNaJl.js} +1 -1
- package/dist/esm/{filepond-plugin-image-edit-D62rKlfH.js → filepond-plugin-image-edit-CDHAxJY8.js} +1 -1
- package/dist/esm/{filepond-plugin-image-exif-orientation-DerapxIk.js → filepond-plugin-image-exif-orientation-BGKiclwK.js} +1 -1
- package/dist/esm/{filepond-plugin-image-preview-BlRd2Ble.js → filepond-plugin-image-preview-BUjvnt8G.js} +1 -1
- package/dist/esm/{index-CtoXzf1y.js → index-ze9e0dxM.js} +2 -2
- package/dist/esm/{intlTelInput-BZz-u5_w.js → intlTelInput-BkYRB6km.js} +1 -1
- package/dist/esm/loader.js +3 -3
- package/dist/esm/salla-accordion-body_2.entry.js +1 -1
- package/dist/esm/salla-accordion_6.entry.js +2 -2
- package/dist/esm/salla-add-product-button_5.entry.js +2 -2
- package/dist/esm/salla-advertisement.entry.js +1 -1
- package/dist/esm/salla-alert_2.entry.js +1 -1
- package/dist/esm/salla-app-install-alert.entry.js +1 -1
- package/dist/esm/salla-applied-filters.entry.js +2 -2
- package/dist/esm/salla-apps-icons.entry.js +1 -1
- package/dist/esm/salla-attachment-view_9.entry.js +11 -11
- package/dist/esm/salla-badge.entry.js +1 -1
- package/dist/esm/salla-basket-gap.entry.js +1 -1
- package/dist/esm/salla-bullet-delivery_3.entry.js +1 -1
- package/dist/esm/salla-cart-coupons.entry.js +1 -1
- package/dist/esm/salla-cart-item-offers_2.entry.js +5 -1
- package/dist/esm/salla-cart-summary-card.entry.js +1 -1
- package/dist/esm/salla-cashback-banner.entry.js +1 -1
- package/dist/esm/salla-comment-factors.entry.js +1 -1
- package/dist/esm/salla-comment-form_9.entry.js +2 -2
- package/dist/esm/salla-conditional-offer.entry.js +628 -19
- package/dist/esm/salla-contacts.entry.js +1 -1
- package/dist/esm/salla-cookies-bar.entry.js +1 -1
- package/dist/esm/salla-count-down.entry.js +1 -1
- package/dist/esm/salla-custom-fields.entry.js +1 -1
- package/dist/esm/salla-delivery-promise.entry.js +1 -1
- package/dist/esm/salla-edit-order-button.entry.js +1 -1
- package/dist/esm/salla-filters-widget.entry.js +2 -2
- package/dist/esm/salla-filters.entry.js +2 -2
- package/dist/esm/salla-free-shipping-banner.entry.js +1 -1
- package/dist/esm/salla-free-shipping-tag.entry.js +1 -1
- package/dist/esm/salla-fulfillment-methods.entry.js +1 -1
- package/dist/esm/salla-gift-card-redeem_2.entry.js +1 -1
- package/dist/esm/salla-gifting.entry.js +1 -1
- package/dist/esm/salla-hook.entry.js +1 -1
- package/dist/esm/salla-infinite-scroll.entry.js +2 -2
- package/dist/esm/salla-installment.entry.js +1 -1
- package/dist/esm/salla-list-tile.entry.js +1 -1
- package/dist/esm/salla-localization-modal.entry.js +1 -1
- package/dist/esm/salla-login-modal.entry.js +1 -1
- package/dist/esm/salla-loyalty-banner_10.entry.js +1 -1
- package/dist/esm/salla-loyalty-exchange-product-card.entry.js +1 -1
- package/dist/esm/salla-loyalty-points-banner.entry.js +1 -1
- package/dist/esm/salla-loyalty-points-history.entry.js +1 -1
- package/dist/esm/salla-loyalty-prize-item.entry.js +1 -1
- package/dist/esm/salla-loyalty.entry.js +1 -1
- package/dist/esm/salla-maintenance-alert.entry.js +1 -1
- package/dist/esm/salla-map_3.entry.js +1 -1
- package/dist/esm/salla-menu.entry.js +1 -1
- package/dist/esm/salla-metadata.entry.js +1 -1
- package/dist/esm/salla-mobile-update-modal.entry.js +1 -1
- package/dist/esm/salla-multiple-bundle-product-cart_2.entry.js +1 -1
- package/dist/esm/salla-multiple-bundle-product-options-modal_2.entry.js +1 -1
- package/dist/esm/salla-multiple-bundle-product.entry.js +1 -1
- package/dist/esm/salla-next-order-coupon.entry.js +8 -1
- package/dist/esm/salla-notification-item_2.entry.js +2 -2
- package/dist/esm/salla-offer-modal.entry.js +1 -1
- package/dist/esm/salla-offer.entry.js +1 -1
- package/dist/esm/salla-order-branch.entry.js +1 -1
- package/dist/esm/salla-order-cancel.entry.js +1 -1
- package/dist/esm/salla-order-details-multiple-bundle-product.entry.js +1 -1
- package/dist/esm/salla-order-details-options.entry.js +1 -1
- package/dist/esm/salla-order-details.entry.js +1 -1
- package/dist/esm/salla-order-edit-item.entry.js +1 -1
- package/dist/esm/salla-order-edit-product-card.entry.js +1 -1
- package/dist/esm/salla-order-edit.entry.js +1 -1
- package/dist/esm/salla-order-items.entry.js +1 -1
- package/dist/esm/salla-order-options-modal.entry.js +1 -1
- package/dist/esm/salla-order-payment.entry.js +1 -1
- package/dist/esm/salla-order-rewards.entry.js +1 -1
- package/dist/esm/salla-order-shipments.entry.js +2 -11
- package/dist/esm/salla-order-summary.entry.js +1 -1
- package/dist/esm/salla-order-totals-card.entry.js +1 -1
- package/dist/esm/salla-orders.entry.js +2 -2
- package/dist/esm/salla-payments.entry.js +1 -1
- package/dist/esm/salla-placeholder.entry.js +1 -1
- package/dist/esm/salla-price-range.entry.js +1 -1
- package/dist/esm/salla-product-card-embed.entry.js +1 -1
- package/dist/esm/salla-product-card.entry.js +1 -1
- package/dist/esm/salla-product-size-guide.entry.js +1 -1
- package/dist/esm/salla-products-list.entry.js +2 -2
- package/dist/esm/salla-products-slider.entry.js +2 -2
- package/dist/esm/salla-quick-order.entry.js +1 -1
- package/dist/esm/salla-rating-modal-v2.entry.js +2 -2
- package/dist/esm/salla-rating-modal.entry.js +2 -2
- package/dist/esm/salla-review-factors-tags.entry.js +1 -1
- package/dist/esm/salla-review-order-item.entry.js +1 -1
- package/dist/esm/salla-scopes.entry.js +1 -1
- package/dist/esm/salla-search-product-card.entry.js +1 -1
- package/dist/esm/salla-search.entry.js +2 -2
- package/dist/esm/salla-skeleton.entry.js +1 -1
- package/dist/esm/salla-social-share.entry.js +1 -1
- package/dist/esm/salla-social.entry.js +1 -1
- package/dist/esm/salla-tab-content_3.entry.js +1 -1
- package/dist/esm/salla-thank-you-header.entry.js +1 -1
- package/dist/esm/salla-thank-you-page.entry.js +1 -1
- package/dist/esm/salla-tiered-offer.entry.js +1 -1
- package/dist/esm/salla-trust-badges.entry.js +1 -1
- package/dist/esm/salla-user-menu.entry.js +1 -1
- package/dist/esm/salla-user-profile.entry.js +1 -1
- package/dist/esm/salla-user-settings.entry.js +1 -1
- package/dist/esm/salla-verify.entry.js +1 -1
- package/dist/esm/salla-wallet.entry.js +1 -1
- package/dist/esm/salla-wishlist-actions.entry.js +1 -1
- package/dist/esm/salla-wishlist-summary.entry.js +2 -2
- package/dist/esm/twilight.js +3 -3
- package/dist/esm/{vanilla-picker-BfmpXY4G.js → vanilla-picker-CxhuwWDi.js} +1 -1
- package/dist/twilight/{p-dc2be7b8.entry.js → p-008740fe.entry.js} +1 -1
- package/dist/twilight/{p-aac9d2fe.entry.js → p-024ecda2.entry.js} +1 -1
- package/dist/twilight/{p-60ae422e.entry.js → p-02b7dfcc.entry.js} +1 -1
- package/dist/twilight/{p-4008e164.entry.js → p-03b2df97.entry.js} +1 -1
- package/dist/twilight/{p-36167cbe.entry.js → p-07e581af.entry.js} +1 -1
- package/dist/twilight/{p-2afc4658.entry.js → p-08be494e.entry.js} +1 -1
- package/dist/twilight/{p-4f4933f8.entry.js → p-0c022fbd.entry.js} +1 -1
- package/dist/twilight/{p-ebe45961.entry.js → p-0c0b2b36.entry.js} +1 -1
- package/dist/twilight/{p-40e651ac.entry.js → p-0fd15433.entry.js} +1 -1
- package/dist/twilight/{p-a4fee391.entry.js → p-11eb8675.entry.js} +1 -1
- package/dist/twilight/{p-f738f492.entry.js → p-12a52f1b.entry.js} +1 -1
- package/dist/twilight/{p-3e285056.entry.js → p-16e2a110.entry.js} +1 -1
- package/dist/twilight/{p-3da2e3f3.entry.js → p-1ca39f1c.entry.js} +1 -1
- package/dist/twilight/{p-fbce8a18.entry.js → p-20949dfb.entry.js} +1 -1
- package/dist/twilight/{p-4080fdc9.entry.js → p-2aa9147c.entry.js} +1 -1
- package/dist/twilight/{p-0b583810.entry.js → p-2dcd24fe.entry.js} +1 -1
- package/dist/twilight/{p-8413d52b.entry.js → p-331003bc.entry.js} +1 -1
- package/dist/twilight/{p-1f39ca4f.entry.js → p-37b64e36.entry.js} +1 -1
- package/dist/twilight/{p-1af75625.entry.js → p-3a2ff4d6.entry.js} +1 -1
- package/dist/twilight/{p-213368ef.entry.js → p-3ec3e9fb.entry.js} +1 -1
- package/dist/twilight/{p-36a7344c.entry.js → p-446e3f38.entry.js} +1 -1
- package/dist/twilight/{p-ed37e702.entry.js → p-455a4702.entry.js} +1 -1
- package/dist/twilight/{p-e89daada.entry.js → p-45a7d444.entry.js} +1 -1
- package/dist/twilight/{p-f3455825.entry.js → p-474d882c.entry.js} +1 -1
- package/dist/twilight/{p-b3472096.entry.js → p-47c79c49.entry.js} +1 -1
- package/dist/twilight/{p-4254e9c2.entry.js → p-4c191ab4.entry.js} +1 -1
- package/dist/twilight/{p-8cbf3bc2.entry.js → p-4cf81a44.entry.js} +1 -1
- package/dist/twilight/{p-97065b4f.entry.js → p-4ec15515.entry.js} +1 -1
- package/dist/twilight/{p-9c1b0978.entry.js → p-519cf4c2.entry.js} +1 -1
- package/dist/twilight/{p-374d9f0c.entry.js → p-57fafaba.entry.js} +1 -1
- package/dist/twilight/{p-a9ae6ab9.entry.js → p-59694c45.entry.js} +1 -1
- package/dist/twilight/{p-5dea6c09.entry.js → p-5b118b5b.entry.js} +1 -1
- package/dist/twilight/{p-7169ba9a.entry.js → p-5ceda3d5.entry.js} +1 -1
- package/dist/twilight/{p-02ddd64c.entry.js → p-5d8937c5.entry.js} +1 -1
- package/dist/twilight/{p-a8102290.entry.js → p-5e0f85ae.entry.js} +1 -1
- package/dist/twilight/{p-b3c9ab0e.entry.js → p-5f9d87ec.entry.js} +1 -1
- package/dist/twilight/{p-034b80ad.entry.js → p-61b98119.entry.js} +1 -1
- package/dist/twilight/{p-f51f31df.entry.js → p-67cf7454.entry.js} +1 -1
- package/dist/twilight/{p-cd55369c.entry.js → p-6d6b99fe.entry.js} +1 -1
- package/dist/twilight/{p-1d105a57.entry.js → p-71ce8e4f.entry.js} +1 -1
- package/dist/twilight/{p-00a1ec4e.entry.js → p-73649c81.entry.js} +1 -1
- package/dist/twilight/{p-a260f35e.entry.js → p-75ad7f14.entry.js} +1 -1
- package/dist/twilight/{p-117e714b.entry.js → p-75f7f1d8.entry.js} +1 -1
- package/dist/twilight/{p-2e1dd9cf.entry.js → p-770142fe.entry.js} +1 -1
- package/dist/twilight/{p-7c684782.entry.js → p-7731e280.entry.js} +1 -1
- package/dist/twilight/p-776f9492.entry.js +4 -0
- package/dist/twilight/{p-365ff361.entry.js → p-7790a9a1.entry.js} +1 -1
- package/dist/twilight/p-78c04993.entry.js +4 -0
- package/dist/twilight/{p-3d3eda0f.entry.js → p-792494c8.entry.js} +1 -1
- package/dist/twilight/{p-c4264e87.entry.js → p-7a985fe3.entry.js} +1 -1
- package/dist/twilight/{p-6495383e.entry.js → p-7fffa509.entry.js} +1 -1
- package/dist/twilight/{p-d76b7564.entry.js → p-8077d8b6.entry.js} +1 -1
- package/dist/twilight/{p-9e50f29c.entry.js → p-830abcc1.entry.js} +1 -1
- package/dist/twilight/{p-3356638d.entry.js → p-89598ad3.entry.js} +1 -1
- package/dist/twilight/{p-0cb60522.entry.js → p-8b0b6bca.entry.js} +1 -1
- package/dist/twilight/{p-8b9c44c8.entry.js → p-8d096f95.entry.js} +1 -1
- package/dist/twilight/{p-a236dd3b.entry.js → p-8d3c5d1f.entry.js} +1 -1
- package/dist/twilight/{p-ae1efde9.entry.js → p-93571975.entry.js} +1 -1
- package/dist/twilight/{p-38b70281.entry.js → p-97c5b11d.entry.js} +1 -1
- package/dist/twilight/{p-c18fe9e5.entry.js → p-97fa91db.entry.js} +1 -1
- package/dist/twilight/{p-94d5b080.entry.js → p-9a2567e2.entry.js} +1 -1
- package/dist/twilight/p-9acfe389.entry.js +4 -0
- package/dist/twilight/{p-79b743eb.entry.js → p-9dd7cf71.entry.js} +1 -1
- package/dist/twilight/{p-2882d626.entry.js → p-9fe5c0f5.entry.js} +1 -1
- package/dist/twilight/{p-F1Erq7GR.js → p-B8nGVDvQ.js} +1 -1
- package/dist/twilight/{p-CC-KFXD9.js → p-BjUh9YCs.js} +1 -1
- package/dist/twilight/{p-BwUX8dew.js → p-BlPdaFn2.js} +1 -1
- package/dist/twilight/{p-6KZIgnh6.js → p-BpJsOjKP.js} +1 -1
- package/dist/twilight/{p-78r0jdyY.js → p-C9ldyQUq.js} +1 -1
- package/dist/twilight/{p-x4_Sxqd_.js → p-CICtuB_u.js} +1 -1
- package/dist/twilight/{p-DT73iyFr.js → p-CNQaYCUJ.js} +1 -1
- package/dist/twilight/{p-DYAn-F48.js → p-DOUAavek.js} +2 -2
- package/dist/twilight/{p-b2cbdaa0.entry.js → p-a1a650ab.entry.js} +1 -1
- package/dist/twilight/{p-71b5546b.entry.js → p-a7566af2.entry.js} +1 -1
- package/dist/twilight/{p-e8198efc.entry.js → p-a7849674.entry.js} +1 -1
- package/dist/twilight/{p-2140be84.entry.js → p-a865995d.entry.js} +1 -1
- package/dist/twilight/{p-8fe950e8.entry.js → p-b297fe2e.entry.js} +1 -1
- package/dist/twilight/{p-e8ebb2fe.entry.js → p-b6098938.entry.js} +1 -1
- package/dist/twilight/{p-0d1b8490.entry.js → p-b71673ee.entry.js} +1 -1
- package/dist/twilight/{p-e2cb5c49.entry.js → p-bb137dbb.entry.js} +1 -1
- package/dist/twilight/{p-6ffbf505.entry.js → p-bd06c200.entry.js} +1 -1
- package/dist/twilight/{p-7303ebd1.entry.js → p-be8c42f1.entry.js} +1 -1
- package/dist/twilight/{p-3845e427.entry.js → p-bf5b5f52.entry.js} +1 -1
- package/dist/twilight/{p-72b0724c.entry.js → p-bf65d2fa.entry.js} +1 -1
- package/dist/twilight/{p-d6a3b2d8.entry.js → p-c0301e00.entry.js} +1 -1
- package/dist/twilight/{p-2c04bba3.entry.js → p-c122e9c1.entry.js} +1 -1
- package/dist/twilight/{p-f67d03fb.entry.js → p-c6f09614.entry.js} +1 -1
- package/dist/twilight/{p-8ea3a265.entry.js → p-cb961f05.entry.js} +1 -1
- package/dist/twilight/{p-e5dc1d27.entry.js → p-cbd056ba.entry.js} +1 -1
- package/dist/twilight/{p-4c5a7ffc.entry.js → p-cdeaf60b.entry.js} +1 -1
- package/dist/twilight/{p-D5DrmlyQ.js → p-cykIAOlk.js} +1 -1
- package/dist/twilight/{p-2566f688.entry.js → p-d089ac6d.entry.js} +1 -1
- package/dist/twilight/{p-ebed682b.entry.js → p-d2396143.entry.js} +1 -1
- package/dist/twilight/{p-974c100d.entry.js → p-d300a117.entry.js} +1 -1
- package/dist/twilight/{p-1b4c3b85.entry.js → p-d61298f8.entry.js} +1 -1
- package/dist/twilight/{p-f664fa69.entry.js → p-d89328d7.entry.js} +1 -1
- package/dist/twilight/p-da0349f9.entry.js +4 -0
- package/dist/twilight/{p-118aa92e.entry.js → p-da60995f.entry.js} +1 -1
- package/dist/twilight/{p-cbff6c68.entry.js → p-dbb4db25.entry.js} +1 -1
- package/dist/twilight/{p-4adaee32.entry.js → p-dceb3725.entry.js} +1 -1
- package/dist/twilight/{p-8fd6ed52.entry.js → p-de10d0c3.entry.js} +1 -1
- package/dist/twilight/{p-77deaa00.entry.js → p-dfde387f.entry.js} +1 -1
- package/dist/twilight/{p-7a8fde62.entry.js → p-e23b84bf.entry.js} +1 -1
- package/dist/twilight/{p-0ce0cdb0.entry.js → p-e3dd9502.entry.js} +1 -1
- package/dist/twilight/{p-b1bc70af.entry.js → p-e849f5ef.entry.js} +1 -1
- package/dist/twilight/{p-f310b923.entry.js → p-ed96c703.entry.js} +1 -1
- package/dist/twilight/{p-2401f0d7.entry.js → p-f054cf8e.entry.js} +1 -1
- package/dist/twilight/{p-aca2717e.entry.js → p-f23ada45.entry.js} +1 -1
- package/dist/twilight/{p-95e7c2e1.entry.js → p-f7c06bee.entry.js} +1 -1
- package/dist/twilight/{p-c1ab1b40.entry.js → p-f9dd6b3e.entry.js} +1 -1
- package/dist/twilight/{p-055bf228.entry.js → p-fb955257.entry.js} +1 -1
- package/dist/twilight/{p-061216d4.entry.js → p-ffc7f526.entry.js} +1 -1
- package/dist/twilight/{p-BeC8AhMn.js → p-wYYV5ykR.js} +1 -1
- package/dist/twilight/{p-CtoXzf1y.js → p-ze9e0dxM.js} +1 -1
- package/dist/twilight/twilight.esm.js +1 -1
- package/dist/types/components/salla-conditional-offer/interfaces.d.ts +36 -0
- package/dist/types/components/salla-conditional-offer/salla-conditional-offer.d.ts +182 -3
- package/dist/types/components/salla-order-shipments/salla-order-shipments.d.ts +0 -1
- package/dist/types/shared/cart-free-gifts.d.ts +26 -0
- package/dist/types/shared/offer-stages.d.ts +6 -1
- package/package.json +5 -5
- package/dist/twilight/p-0fb390fb.entry.js +0 -4
- package/dist/twilight/p-17be7967.entry.js +0 -4
- package/dist/twilight/p-5bc2190a.entry.js +0 -4
- package/dist/twilight/p-919650b7.entry.js +0 -4
|
@@ -45,6 +45,9 @@ var OfferType;
|
|
|
45
45
|
* `twilight/packages/twilight/src/api/cart.js:831-846`.
|
|
46
46
|
*/
|
|
47
47
|
const EMPTY_GIFT_ITEMS = Object.freeze([]);
|
|
48
|
+
const EMPTY_PRODUCT_LINES = Object.freeze([]);
|
|
49
|
+
/** Every line from the last parsed response — published in the SAME pass as `itemsCache`. */
|
|
50
|
+
let linesCache = null;
|
|
48
51
|
/**
|
|
49
52
|
* The gift LINES from the last parsed response. `null` is the single "nothing has loaded
|
|
50
53
|
* yet" flag shared with `countCache`, which keeps the two from ever disagreeing about
|
|
@@ -146,6 +149,7 @@ function syncCartIdentity() {
|
|
|
146
149
|
if (cacheIdentity !== undefined && cacheIdentity !== current) {
|
|
147
150
|
generation++;
|
|
148
151
|
itemsCache = null;
|
|
152
|
+
linesCache = null;
|
|
149
153
|
countCache = null;
|
|
150
154
|
isAuthoritative = false;
|
|
151
155
|
pendingRequest = null;
|
|
@@ -270,6 +274,24 @@ function buildGiftItems(items) {
|
|
|
270
274
|
}
|
|
271
275
|
return gifts;
|
|
272
276
|
}
|
|
277
|
+
/** Every line of the cart, in cart order — see `CartProductLine`. */
|
|
278
|
+
function buildProductLines(items) {
|
|
279
|
+
const lines = [];
|
|
280
|
+
for (const item of items) {
|
|
281
|
+
if (item?.id === undefined || item?.id === null)
|
|
282
|
+
continue;
|
|
283
|
+
const quantity = toNumber(item.quantity);
|
|
284
|
+
lines.push({
|
|
285
|
+
id: String(item.id),
|
|
286
|
+
productId: toNullableNumber(item.product_id),
|
|
287
|
+
quantity,
|
|
288
|
+
// Only a line the server flagged as granted by a cart offer has free units.
|
|
289
|
+
freeQuantity: item.added_by_offer ? Math.max(0, resolveFreeQuantity(item, quantity)) : 0,
|
|
290
|
+
offerId: item.added_by_offer ? toNullableNumber(item.offer_id) : null,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return lines;
|
|
294
|
+
}
|
|
273
295
|
function fetchAndCache() {
|
|
274
296
|
if (pendingRequest)
|
|
275
297
|
return pendingRequest;
|
|
@@ -284,6 +306,7 @@ function fetchAndCache() {
|
|
|
284
306
|
// Both projections come out of the SAME response, in one pass.
|
|
285
307
|
return {
|
|
286
308
|
gifts: buildGiftItems(items),
|
|
309
|
+
lines: buildProductLines(items),
|
|
287
310
|
count: extractCartCount(res, found),
|
|
288
311
|
// The response was reached and parsed — these values are the cart, so the cache
|
|
289
312
|
// they land in may be short-circuited on. See `isAuthoritative`.
|
|
@@ -303,6 +326,7 @@ function fetchAndCache() {
|
|
|
303
326
|
// connection can never claim the cart is empty.
|
|
304
327
|
.catch((error) => ({
|
|
305
328
|
gifts: itemsCache ?? [],
|
|
329
|
+
lines: linesCache ?? [],
|
|
306
330
|
count: isCartNotCreated(error) ? 0 : countCache,
|
|
307
331
|
/**
|
|
308
332
|
* NOT authoritative, `CartNotCreated` included.
|
|
@@ -319,13 +343,14 @@ function fetchAndCache() {
|
|
|
319
343
|
*/
|
|
320
344
|
ok: false,
|
|
321
345
|
}))
|
|
322
|
-
.then(({ gifts, count, ok }) => {
|
|
346
|
+
.then(({ gifts, lines, count, ok }) => {
|
|
323
347
|
// Only publish if nothing invalidated us while we were in flight.
|
|
324
348
|
// Otherwise this is pre-mutation data racing a newer, authoritative
|
|
325
349
|
// request — publishing it would make the lookups short-circuit on stale
|
|
326
350
|
// data until the NEXT mutation happens to come along.
|
|
327
351
|
if (startedAtGeneration === generation) {
|
|
328
352
|
itemsCache = gifts;
|
|
353
|
+
linesCache = lines;
|
|
329
354
|
countCache = count;
|
|
330
355
|
isAuthoritative = ok;
|
|
331
356
|
}
|
|
@@ -354,6 +379,20 @@ function getCartFreeGiftItems() {
|
|
|
354
379
|
}
|
|
355
380
|
return fetchAndCache().then(() => itemsCache ?? EMPTY_GIFT_ITEMS);
|
|
356
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* Every line of the cart (paid and free), in cart order — for the CP-2171 free product picker.
|
|
384
|
+
*
|
|
385
|
+
* Shares the cache, the in-flight promise and the generation guard with
|
|
386
|
+
* `getCartFreeGiftItems()` and `getCartItemCount()`: no request of its own, and never a
|
|
387
|
+
* different cart state from theirs.
|
|
388
|
+
*/
|
|
389
|
+
function getCartProductLines() {
|
|
390
|
+
syncCartIdentity();
|
|
391
|
+
if (isAuthoritative && itemsCache) {
|
|
392
|
+
return Promise.resolve(linesCache ?? EMPTY_PRODUCT_LINES);
|
|
393
|
+
}
|
|
394
|
+
return fetchAndCache().then(() => linesCache ?? EMPTY_PRODUCT_LINES);
|
|
395
|
+
}
|
|
357
396
|
/**
|
|
358
397
|
* Whether the cached cart picture came from a response that was actually reached and
|
|
359
398
|
* parsed, rather than from the fallback a rejected lookup publishes.
|
|
@@ -426,6 +465,7 @@ function refreshCartFreeGifts() {
|
|
|
426
465
|
return pendingRefresh;
|
|
427
466
|
generation++;
|
|
428
467
|
itemsCache = null;
|
|
468
|
+
linesCache = null;
|
|
429
469
|
countCache = null;
|
|
430
470
|
isAuthoritative = false;
|
|
431
471
|
// This invalidation already covers whatever id the cart has now.
|
|
@@ -664,8 +704,13 @@ function sortByThreshold(items, getThreshold, getIdentity) {
|
|
|
664
704
|
* @param rawTierValues each stage's OWN `current_value`, aligned the same way. A tier
|
|
665
705
|
* without a readable one is measured by `currentValue`, so omitting it — an older
|
|
666
706
|
* backend — changes nothing. See `OfferStageProgress.values`.
|
|
707
|
+
* @param rawCanAddFree each stage's `can_add_free` (CP-2171), aligned the same way. Read
|
|
708
|
+
* only in case (3), where no tier carries `is_applied`: a free product tier that
|
|
709
|
+
* carries one is reached when the ENGINE says so, not when its `current_value` —
|
|
710
|
+
* which counts the gift lines at full price — passes `min_spend`. Omit it and case
|
|
711
|
+
* (3) is the threshold behaviour, unchanged.
|
|
667
712
|
*/
|
|
668
|
-
function computeOfferStageProgress(currentValue, rawThresholds, rawStartValue = 0, rawApplied = [], rawTierValues = []) {
|
|
713
|
+
function computeOfferStageProgress(currentValue, rawThresholds, rawStartValue = 0, rawApplied = [], rawTierValues = [], rawCanAddFree = []) {
|
|
669
714
|
// Unparseable thresholds are dropped, not zeroed — see `isUsableThreshold`.
|
|
670
715
|
//
|
|
671
716
|
// The flag travels WITH its threshold through both the drop and the sort. Sorting two
|
|
@@ -678,6 +723,7 @@ function computeOfferStageProgress(currentValue, rawThresholds, rawStartValue =
|
|
|
678
723
|
threshold: parseNumeric(threshold),
|
|
679
724
|
applied: readAppliedFlag(rawApplied?.[index]),
|
|
680
725
|
tierValue: parseNumeric(rawTierValues?.[index]),
|
|
726
|
+
canAddFree: typeof rawCanAddFree?.[index] === 'boolean' ? rawCanAddFree[index] : undefined,
|
|
681
727
|
}))
|
|
682
728
|
.filter((entry) => entry.threshold !== null)
|
|
683
729
|
.sort((a, b) => a.threshold - b.threshold || a.index - b.index);
|
|
@@ -765,12 +811,25 @@ function computeOfferStageProgress(currentValue, rawThresholds, rawStartValue =
|
|
|
765
811
|
appliedIndex = index;
|
|
766
812
|
}
|
|
767
813
|
}
|
|
768
|
-
// Case (3) closes the prefix at
|
|
814
|
+
// Case (3) closes the prefix at the last reached tier rather than comparing tier by tier.
|
|
769
815
|
// With one value for every tier the two are the same statement; with per-tier values
|
|
770
816
|
// it is what keeps `met` a prefix, on the same single-winner reasoning as case (1).
|
|
817
|
+
//
|
|
818
|
+
// CP-2171 — a tier that carries `can_add_free` is reached on the engine's answer, not on
|
|
819
|
+
// its value: a guest with 40 paid and three listed gifts in the cart has a value past
|
|
820
|
+
// `min_spend`, yet every gift is charged. Only `met` reads it; the bar (`lastReachedIndex`,
|
|
821
|
+
// `fills`, `isComplete`) stays the threshold projection, and the guidance line, finding
|
|
822
|
+
// nothing left to spend, says the conditions are not met.
|
|
823
|
+
let lastMetIndex = NO_STAGE_REACHED;
|
|
824
|
+
for (let index = 0; index < thresholds.length; index++) {
|
|
825
|
+
const canAddFree = entries[index].canAddFree;
|
|
826
|
+
if (canAddFree === undefined ? values[index] >= thresholds[index] : canAddFree) {
|
|
827
|
+
lastMetIndex = index;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
771
830
|
const met = hasEngineVerdict
|
|
772
831
|
? thresholds.map((_, index) => index <= appliedIndex)
|
|
773
|
-
: thresholds.map((_, index) => index <=
|
|
832
|
+
: thresholds.map((_, index) => index <= lastMetIndex);
|
|
774
833
|
const metCount = met.filter(Boolean).length;
|
|
775
834
|
const nextUnmetIndex = met.findIndex(isMet => !isMet); // -1 === NO_STAGE_REACHED
|
|
776
835
|
const allMet = nextUnmetIndex === NO_STAGE_REACHED;
|
|
@@ -918,7 +977,7 @@ function getRemainingToStage(index, progress, measure = OfferStageMeasure.CURREN
|
|
|
918
977
|
return rounded > 0 ? rounded : 1 / factor;
|
|
919
978
|
}
|
|
920
979
|
|
|
921
|
-
const sallaConditionalOfferCss = "/*!twlt*/:host{display:block}.s-conditional-offer-root{background-color:rgba(0, 137, 123, 0.06);background-color:color-mix(in srgb, var(--color-primary, #00897b) 7%, transparent)}.s-conditional-offer-badges::-webkit-scrollbar{height:3px}.s-conditional-offer-badges::-webkit-scrollbar-thumb{border-radius:9999px;background-color:rgba(0, 0, 0, 0.15)}.s-conditional-offer-badges::-webkit-scrollbar-track{background-color:transparent}.s-conditional-offer-tooltip{--s-conditional-offer-tooltip-shift:0px;--s-conditional-offer-tooltip-caret:0px;transform:translateX(calc(-50% + var(--s-conditional-offer-tooltip-shift)))}.s-conditional-offer-tooltip::before{content:\"\";position:absolute;bottom:100%;left:50%;transform:translateX(calc(-50% + var(--s-conditional-offer-tooltip-caret)));border:8px solid transparent;border-bottom-color:#fff}.s-conditional-offer-tier-icon-met,.s-conditional-offer-tier-icon-unmet{color:var(--color-primary-dark, #00695c);background-color:rgba(0, 137, 123, 0.12);background-color:color-mix(in srgb, var(--color-primary, #00897b) 14%, transparent)}.s-conditional-offer-tier:not(:last-child)::before{content:\"\";position:absolute;top:2.5rem;bottom:-1rem;inset-inline-start:1.125rem;width:1px;background-color:#d1d5db}.visible{visibility:visible}.invisible{visibility:hidden}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.block{display:block}.inline{display:inline}.flex{display:flex}.hidden{display:none}.w-6{width:1.5rem}.resize{resize:both}.gap-1{gap:0.25rem}.rounded{border-radius:var(--s-radius, 0.25rem)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.s-conditional-offer-root{margin-bottom:1.25rem;display:block;width:100%;min-width:0px;max-width:100%;border-radius:0.375rem;padding:0.75rem}@media (min-width: 768px){.s-conditional-offer-root{padding:1rem}}.s-conditional-offer-banner{display:flex;width:100%;min-width:0px;align-items:center;justify-content:space-between;gap:0.75rem}@media (min-width: 768px){.s-conditional-offer-banner{gap:1rem}}.s-conditional-offer-lead{display:flex;min-width:0px;flex:1 1 0%;align-items:center;gap:0.5rem}@media (min-width: 768px){.s-conditional-offer-lead{gap:0.75rem}}.s-conditional-offer-badges{display:flex;max-width:45%;flex-shrink:0;flex-wrap:nowrap;align-items:center;gap:0.25rem;overflow-x:auto;overscroll-behavior-x:contain;scroll-behavior:smooth;scrollbar-width:thin}.s-conditional-offer-badge{display:inline-flex;height:1.5rem;width:1.5rem;flex-shrink:0;align-items:center;justify-content:center;border-radius:9999px;font-size:11px;line-height:1;--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.s-conditional-offer-badge-met{background-color:var(--color-primary)}.s-conditional-offer-badge-unmet{--tw-bg-opacity:1;background-color:rgb(156 163 175 / var(--tw-bg-opacity, 1))}.s-conditional-offer-badge-link{width:0.5rem;flex-shrink:0;border-top-width:1px}.s-conditional-offer-badge-link-met{border-color:var(--color-primary)}.s-conditional-offer-badge-link-unmet{border-style:dashed;--tw-border-opacity:1;border-color:rgb(82 82 82 / var(--tw-border-opacity, 1))}.s-conditional-offer-badge-icon{height:0.75rem;width:0.75rem;flex-shrink:0}.s-conditional-offer-text{display:flex;min-width:0px;flex-direction:column;gap:0.125rem;text-align:start}.s-conditional-offer-heading{display:flex;min-width:0px;align-items:center;gap:0.375rem}.s-conditional-offer-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:0.75rem;line-height:1rem;--tw-text-opacity:1;color:rgb(82 82 82 / var(--tw-text-opacity, 1))}.s-conditional-offer-info{position:relative;display:inline-flex;flex-shrink:0;align-items:center}.s-conditional-offer-info-trigger{display:inline-flex;height:1rem;width:1rem;flex-shrink:0;align-items:center;justify-content:center;border-radius:9999px;--tw-text-opacity:1;color:rgb(115 115 115 / var(--tw-text-opacity, 1));transition-property:color, background-color, border-color, text-decoration-color, fill, stroke;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:300ms}.s-conditional-offer-info-trigger:hover{color:var(--color-primary-dark)}.s-conditional-offer-info-icon{height:0.875rem;width:0.875rem;flex-shrink:0}.s-conditional-offer-message{font-size:0.875rem;line-height:1.25rem;font-weight:700;color:var(--color-primary-dark)}@media (min-width: 768px){.s-conditional-offer-message{font-size:1rem;line-height:1.5rem}}.s-conditional-offer-toggle{align-items:center;justify-content:center;gap:0.375rem;font-size:0.875rem;line-height:1.25rem;--tw-text-opacity:1;color:rgb(38 38 38 / var(--tw-text-opacity, 1));transition-property:color, background-color, border-color, text-decoration-color, fill, stroke;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:300ms}.s-conditional-offer-toggle:hover{color:var(--color-primary-dark)}.s-conditional-offer-toggle-inline{display:none;flex-shrink:0}@media (min-width: 768px){.s-conditional-offer-toggle-inline{display:inline-flex}}.s-conditional-offer-toggle-bar{margin-top:0.5rem;display:inline-flex;width:100%;border-radius:0.375rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(229 231 235 / var(--tw-border-opacity, 1));--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));padding-left:0.75rem;padding-right:0.75rem;padding-top:0.625rem;padding-bottom:0.625rem}@media (min-width: 768px){.s-conditional-offer-toggle-bar{display:none}}.s-conditional-offer-toggle-icon{font-size:1rem;line-height:1.5rem;line-height:1;transition-property:transform;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:300ms}.s-conditional-offer-toggle-icon-open{--tw-rotate:180deg;transform:translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.s-conditional-offer-panel{margin-top:0.75rem;width:100%;min-width:0px;border-radius:0.375rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(243 244 246 / var(--tw-border-opacity, 1));--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));padding:0.75rem}.s-conditional-offer-gifts-header{margin-bottom:0.75rem;display:flex;align-items:center;gap:0.5rem}.s-conditional-offer-gifts-icon{display:inline-flex;height:2rem;width:2rem;flex-shrink:0;align-items:center;justify-content:center;border-radius:9999px;border-width:1px;border-color:var(--color-primary);font-size:1rem;line-height:1.5rem;line-height:1;color:var(--color-primary)}.s-conditional-offer-gifts-title{font-size:0.875rem;line-height:1.25rem;font-weight:700;color:var(--color-primary-dark)}.s-conditional-offer-gifts-list{margin:0px;display:flex;list-style-type:none;flex-direction:column;gap:0.5rem;padding:0px}.s-conditional-offer-gift{display:flex;align-items:center;gap:0.75rem;border-radius:0.375rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(243 244 246 / var(--tw-border-opacity, 1));padding:0.5rem}.s-conditional-offer-gift-thumb{display:inline-flex;height:3.5rem;width:3.5rem;flex-shrink:0;align-items:center;justify-content:center;overflow:hidden;border-radius:0.375rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(243 244 246 / var(--tw-border-opacity, 1));--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));font-size:1.25rem;line-height:1.75rem;--tw-text-opacity:1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.s-conditional-offer-gift-image{height:100%;width:100%;object-fit:contain}.s-conditional-offer-gift-body{display:flex;min-width:0px;flex:1 1 0%;flex-direction:column;gap:0.125rem;text-align:start}.s-conditional-offer-gift-name{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;font-size:0.875rem;line-height:1.25rem;font-weight:700;--tw-text-opacity:1;color:rgb(38 38 38 / var(--tw-text-opacity, 1))}.s-conditional-offer-gift-prices{display:flex;align-items:center;gap:0.5rem;font-size:0.75rem;line-height:1rem}.s-conditional-offer-gift-free{font-weight:700;--tw-text-opacity:1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.s-conditional-offer-gift-price{--tw-text-opacity:1;color:rgb(115 115 115 / var(--tw-text-opacity, 1));text-decoration-line:line-through}.s-conditional-offer-tooltip{position:absolute;top:calc(100% + 0.75rem);left:50%;z-index:10;width:min(22rem, calc(100vw - 2rem));border-radius:0.5rem;--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));padding:1rem;text-align:start;--tw-shadow:5px 10px 30px #2B2D340D;--tw-shadow-colored:5px 10px 30px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.s-conditional-offer-tooltip-title{font-size:0.875rem;line-height:1.25rem;font-weight:700;color:var(--color-primary-dark)}.s-conditional-offer-tooltip-description{margin-top:0.25rem;font-size:0.75rem;line-height:1rem;line-height:1.625;--tw-text-opacity:1;color:rgb(82 82 82 / var(--tw-text-opacity, 1))}.s-conditional-offer-tooltip-list{margin:0px;margin-top:0.75rem;display:flex;list-style-type:none;flex-direction:column;gap:1rem;padding:0px}.s-conditional-offer-tier{position:relative;display:flex;align-items:flex-start;gap:0.75rem}.s-conditional-offer-tier-icon{position:relative;display:inline-flex;height:2.25rem;width:2.25rem;flex-shrink:0;align-items:center;justify-content:center;border-radius:9999px;font-size:1rem;line-height:1.5rem;line-height:1}.s-conditional-offer-tier-icon-unmet{color:var(--color-primary-dark)}.s-conditional-offer-tier-status{position:absolute;bottom:-0.125rem;display:inline-flex;height:1rem;width:1rem;align-items:center;justify-content:center;border-radius:9999px;border-width:2px;--tw-border-opacity:1;border-color:rgb(255 255 255 / var(--tw-border-opacity, 1));--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));inset-inline-start:-2px}.s-conditional-offer-tier-status-met{color:var(--color-primary-dark)}.s-conditional-offer-tier-status-unmet{--tw-text-opacity:1;color:rgb(163 163 163 / var(--tw-text-opacity, 1))}.s-conditional-offer-tier-status-check{height:0.75rem;width:0.75rem;flex-shrink:0}.s-conditional-offer-tier-status-lock{height:0.625rem;width:0.625rem;flex-shrink:0}.s-conditional-offer-tier-body{display:flex;min-width:0px;flex-direction:column;gap:0.125rem}.s-conditional-offer-tier-title{font-size:0.75rem;line-height:1rem;font-weight:700;color:var(--color-primary-dark)}.s-conditional-offer-tier-text{font-size:0.75rem;line-height:1rem;line-height:1.625;--tw-text-opacity:1;color:rgb(82 82 82 / var(--tw-text-opacity, 1))}.s-conditional-offer-tier-text-met{text-decoration-line:line-through}.s-conditional-offer-skeleton-text{display:flex;min-width:0px;flex:1 1 0%;flex-direction:column;gap:0.375rem}";
|
|
980
|
+
const sallaConditionalOfferCss = "/*!twlt*/:host{display:block}.s-conditional-offer-root{background-color:rgba(0, 137, 123, 0.06);background-color:color-mix(in srgb, var(--color-primary, #00897b) 7%, transparent)}.s-conditional-offer-badges::-webkit-scrollbar{height:3px}.s-conditional-offer-badges::-webkit-scrollbar-thumb{border-radius:9999px;background-color:rgba(0, 0, 0, 0.15)}.s-conditional-offer-badges::-webkit-scrollbar-track{background-color:transparent}.s-conditional-offer-tooltip{--s-conditional-offer-tooltip-shift:0px;--s-conditional-offer-tooltip-caret:0px;transform:translateX(calc(-50% + var(--s-conditional-offer-tooltip-shift)))}.s-conditional-offer-tooltip::before{content:\"\";position:absolute;bottom:100%;left:50%;transform:translateX(calc(-50% + var(--s-conditional-offer-tooltip-caret)));border:8px solid transparent;border-bottom-color:#fff}.s-conditional-offer-tier-icon-met,.s-conditional-offer-tier-icon-unmet{color:var(--color-primary-dark, #00695c);background-color:rgba(0, 137, 123, 0.12);background-color:color-mix(in srgb, var(--color-primary, #00897b) 14%, transparent)}.s-conditional-offer-tier:not(:last-child)::before{content:\"\";position:absolute;top:2.5rem;bottom:-1rem;inset-inline-start:1.125rem;width:1px;background-color:#d1d5db}.visible{visibility:visible}.invisible{visibility:hidden}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.block{display:block}.inline{display:inline}.flex{display:flex}.hidden{display:none}.w-6{width:1.5rem}.resize{resize:both}.gap-1{gap:0.25rem}.rounded{border-radius:var(--s-radius, 0.25rem)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.s-conditional-offer-root{margin-bottom:1.25rem;display:block;width:100%;min-width:0px;max-width:100%;border-radius:0.375rem;padding:0.75rem}@media (min-width: 768px){.s-conditional-offer-root{padding:1rem}}.s-conditional-offer-banner{display:flex;width:100%;min-width:0px;align-items:center;justify-content:space-between;gap:0.75rem}@media (min-width: 768px){.s-conditional-offer-banner{gap:1rem}}.s-conditional-offer-lead{display:flex;min-width:0px;flex:1 1 0%;align-items:center;gap:0.5rem}@media (min-width: 768px){.s-conditional-offer-lead{gap:0.75rem}}.s-conditional-offer-badges{display:flex;max-width:45%;flex-shrink:0;flex-wrap:nowrap;align-items:center;gap:0.25rem;overflow-x:auto;overscroll-behavior-x:contain;scroll-behavior:smooth;scrollbar-width:thin}.s-conditional-offer-badge{display:inline-flex;height:1.5rem;width:1.5rem;flex-shrink:0;align-items:center;justify-content:center;border-radius:9999px;font-size:11px;line-height:1;--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.s-conditional-offer-badge-met{background-color:var(--color-primary)}.s-conditional-offer-badge-unmet{--tw-bg-opacity:1;background-color:rgb(156 163 175 / var(--tw-bg-opacity, 1))}.s-conditional-offer-badge-link{width:0.5rem;flex-shrink:0;border-top-width:1px}.s-conditional-offer-badge-link-met{border-color:var(--color-primary)}.s-conditional-offer-badge-link-unmet{border-style:dashed;--tw-border-opacity:1;border-color:rgb(82 82 82 / var(--tw-border-opacity, 1))}.s-conditional-offer-badge-icon{height:0.75rem;width:0.75rem;flex-shrink:0}.s-conditional-offer-text{display:flex;min-width:0px;flex-direction:column;gap:0.125rem;text-align:start}.s-conditional-offer-heading{display:flex;min-width:0px;align-items:center;gap:0.375rem}.s-conditional-offer-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:0.75rem;line-height:1rem;--tw-text-opacity:1;color:rgb(82 82 82 / var(--tw-text-opacity, 1))}.s-conditional-offer-info{position:relative;display:inline-flex;flex-shrink:0;align-items:center}.s-conditional-offer-info-trigger{display:inline-flex;height:1rem;width:1rem;flex-shrink:0;align-items:center;justify-content:center;border-radius:9999px;--tw-text-opacity:1;color:rgb(115 115 115 / var(--tw-text-opacity, 1));transition-property:color, background-color, border-color, text-decoration-color, fill, stroke;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:300ms}.s-conditional-offer-info-trigger:hover{color:var(--color-primary-dark)}.s-conditional-offer-info-icon{height:0.875rem;width:0.875rem;flex-shrink:0}.s-conditional-offer-message{font-size:0.875rem;line-height:1.25rem;font-weight:700;color:var(--color-primary-dark)}@media (min-width: 768px){.s-conditional-offer-message{font-size:1rem;line-height:1.5rem}}.s-conditional-offer-toggle{align-items:center;justify-content:center;gap:0.375rem;font-size:0.875rem;line-height:1.25rem;--tw-text-opacity:1;color:rgb(38 38 38 / var(--tw-text-opacity, 1));transition-property:color, background-color, border-color, text-decoration-color, fill, stroke;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:300ms}.s-conditional-offer-toggle:hover{color:var(--color-primary-dark)}.s-conditional-offer-toggle-inline{display:none;flex-shrink:0}@media (min-width: 768px){.s-conditional-offer-toggle-inline{display:inline-flex}}.s-conditional-offer-toggle-bar{margin-top:0.5rem;display:inline-flex;width:100%;border-radius:0.375rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(229 231 235 / var(--tw-border-opacity, 1));--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));padding-left:0.75rem;padding-right:0.75rem;padding-top:0.625rem;padding-bottom:0.625rem}@media (min-width: 768px){.s-conditional-offer-toggle-bar{display:none}}.s-conditional-offer-toggle-icon{font-size:1rem;line-height:1.5rem;line-height:1;transition-property:transform;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:300ms}.s-conditional-offer-toggle-icon-open{--tw-rotate:180deg;transform:translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.s-conditional-offer-panel{margin-top:0.75rem;width:100%;min-width:0px;border-radius:0.375rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(243 244 246 / var(--tw-border-opacity, 1));--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));padding:0.75rem}.s-conditional-offer-gifts-header{margin-bottom:0.75rem;display:flex;align-items:center;gap:0.5rem}.s-conditional-offer-gifts-icon{display:inline-flex;height:2rem;width:2rem;flex-shrink:0;align-items:center;justify-content:center;border-radius:9999px;border-width:1px;border-color:var(--color-primary);font-size:1rem;line-height:1.5rem;line-height:1;color:var(--color-primary)}.s-conditional-offer-gifts-title{font-size:0.875rem;line-height:1.25rem;font-weight:700;color:var(--color-primary-dark)}.s-conditional-offer-gifts-list{margin:0px;display:flex;list-style-type:none;flex-direction:column;gap:0.5rem;padding:0px}.s-conditional-offer-gift{display:flex;align-items:center;gap:0.75rem;border-radius:0.375rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(243 244 246 / var(--tw-border-opacity, 1));padding:0.5rem}.s-conditional-offer-gift-thumb{display:inline-flex;height:3.5rem;width:3.5rem;flex-shrink:0;align-items:center;justify-content:center;overflow:hidden;border-radius:0.375rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(243 244 246 / var(--tw-border-opacity, 1));--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));font-size:1.25rem;line-height:1.75rem;--tw-text-opacity:1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.s-conditional-offer-gift-image{height:100%;width:100%;object-fit:contain}.s-conditional-offer-gift-body{display:flex;min-width:0px;flex:1 1 0%;flex-direction:column;gap:0.125rem;text-align:start}.s-conditional-offer-gift-name{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;font-size:0.875rem;line-height:1.25rem;font-weight:700;--tw-text-opacity:1;color:rgb(38 38 38 / var(--tw-text-opacity, 1))}.s-conditional-offer-gift-prices{display:flex;align-items:center;gap:0.5rem;font-size:0.75rem;line-height:1rem}.s-conditional-offer-gift-free{font-weight:700;--tw-text-opacity:1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.s-conditional-offer-gift-price{--tw-text-opacity:1;color:rgb(115 115 115 / var(--tw-text-opacity, 1));text-decoration-line:line-through}.s-conditional-offer-gifts{margin-top:0.75rem;width:100%;min-width:0px;border-radius:0.375rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(243 244 246 / var(--tw-border-opacity, 1));--tw-bg-opacity:1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1));padding:0.75rem}.s-conditional-offer-picker{display:flex;width:100%;min-width:0px;flex-direction:column;gap:0.75rem}.s-conditional-offer-picker-header{display:flex;align-items:center;gap:0.5rem}.s-conditional-offer-picker-counter{display:inline-flex;height:2.25rem;width:2.25rem;flex-shrink:0;align-items:center;justify-content:center;border-radius:9999px;border-width:2px;border-color:var(--color-primary);font-size:0.75rem;line-height:1rem;font-weight:700;color:var(--color-primary-dark)}.s-conditional-offer-picker-title{font-size:0.875rem;line-height:1.25rem;font-weight:700;color:var(--color-primary-dark)}.s-conditional-offer-picker-list{margin:0px;display:flex;list-style-type:none;gap:0.75rem;overflow-x:auto;overscroll-behavior-x:contain;scroll-behavior:smooth;padding:0px;padding-bottom:0.25rem;scrollbar-width:thin}.s-conditional-offer-picker-card{display:flex;width:9rem;flex-shrink:0;flex-direction:column;gap:0.375rem;text-align:start}.s-conditional-offer-picker-thumb{display:inline-flex;height:7rem;width:100%;align-items:center;justify-content:center;overflow:hidden;border-radius:0.375rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(243 244 246 / var(--tw-border-opacity, 1));--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));font-size:1.5rem;line-height:2rem;--tw-text-opacity:1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.s-conditional-offer-picker-image{height:100%;width:100%;object-fit:contain}.s-conditional-offer-picker-name{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:1;font-size:0.875rem;line-height:1.25rem;--tw-text-opacity:1;color:rgb(38 38 38 / var(--tw-text-opacity, 1))}.s-conditional-offer-picker-prices{display:flex;align-items:center;gap:0.5rem;font-size:0.75rem;line-height:1rem}.s-conditional-offer-picker-price-regular{font-weight:700;--tw-text-opacity:1;color:rgb(23 23 23 / var(--tw-text-opacity, 1))}.s-conditional-offer-picker-actions{margin-top:auto;display:flex;width:100%;align-items:center;gap:0.375rem}.s-conditional-offer-picker-add{display:inline-flex;height:2.25rem;flex:1 1 0%;align-items:center;justify-content:center;border-radius:0.375rem;border-width:1px;border-color:var(--color-primary);font-size:0.875rem;line-height:1.25rem;font-weight:700;color:var(--color-primary-dark);transition-property:color, background-color, border-color, text-decoration-color, fill, stroke;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:300ms}.s-conditional-offer-picker-add:hover{background-color:var(--color-primary);--tw-text-opacity:1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.s-conditional-offer-tooltip{position:absolute;top:calc(100% + 0.75rem);left:50%;z-index:10;width:min(22rem, calc(100vw - 2rem));border-radius:0.5rem;--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));padding:1rem;text-align:start;--tw-shadow:5px 10px 30px #2B2D340D;--tw-shadow-colored:5px 10px 30px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.s-conditional-offer-tooltip-title{font-size:0.875rem;line-height:1.25rem;font-weight:700;color:var(--color-primary-dark)}.s-conditional-offer-tooltip-description{margin-top:0.25rem;font-size:0.75rem;line-height:1rem;line-height:1.625;--tw-text-opacity:1;color:rgb(82 82 82 / var(--tw-text-opacity, 1))}.s-conditional-offer-tooltip-list{margin:0px;margin-top:0.75rem;display:flex;list-style-type:none;flex-direction:column;gap:1rem;padding:0px}.s-conditional-offer-tier{position:relative;display:flex;align-items:flex-start;gap:0.75rem}.s-conditional-offer-tier-icon{position:relative;display:inline-flex;height:2.25rem;width:2.25rem;flex-shrink:0;align-items:center;justify-content:center;border-radius:9999px;font-size:1rem;line-height:1.5rem;line-height:1}.s-conditional-offer-tier-icon-unmet{color:var(--color-primary-dark)}.s-conditional-offer-tier-status{position:absolute;bottom:-0.125rem;display:inline-flex;height:1rem;width:1rem;align-items:center;justify-content:center;border-radius:9999px;border-width:2px;--tw-border-opacity:1;border-color:rgb(255 255 255 / var(--tw-border-opacity, 1));--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));inset-inline-start:-2px}.s-conditional-offer-tier-status-met{color:var(--color-primary-dark)}.s-conditional-offer-tier-status-unmet{--tw-text-opacity:1;color:rgb(163 163 163 / var(--tw-text-opacity, 1))}.s-conditional-offer-tier-status-check{height:0.75rem;width:0.75rem;flex-shrink:0}.s-conditional-offer-tier-status-lock{height:0.625rem;width:0.625rem;flex-shrink:0}.s-conditional-offer-tier-body{display:flex;min-width:0px;flex-direction:column;gap:0.125rem}.s-conditional-offer-tier-title{font-size:0.75rem;line-height:1rem;font-weight:700;color:var(--color-primary-dark)}.s-conditional-offer-tier-text{font-size:0.75rem;line-height:1rem;line-height:1.625;--tw-text-opacity:1;color:rgb(82 82 82 / var(--tw-text-opacity, 1))}.s-conditional-offer-tier-text-met{text-decoration-line:line-through}.s-conditional-offer-skeleton-text{display:flex;min-width:0px;flex:1 1 0%;flex-direction:column;gap:0.375rem}";
|
|
922
981
|
|
|
923
982
|
/** Root of every translation key this component uses. */
|
|
924
983
|
const LANG = "blocks.cart_offer";
|
|
@@ -983,6 +1042,19 @@ const RELOCATION_ANCESTOR_LIMIT = 3;
|
|
|
983
1042
|
* DOM with no cart in it. Past this deadline the full-width banner is the final answer.
|
|
984
1043
|
*/
|
|
985
1044
|
const RELOCATION_WATCH_MS = 5000;
|
|
1045
|
+
/**
|
|
1046
|
+
* CP-2171 — how long the picker stays locked after an add succeeded when no fresh cart
|
|
1047
|
+
* lines arrive (a missed `cart::updated`). Long enough to cover the coalescing window and the
|
|
1048
|
+
* cart lookup; short enough that a lost event never leaves Add ignoring taps for long. Past it
|
|
1049
|
+
* the lock goes and the lines are read again — see `unlockOnStaleLines`.
|
|
1050
|
+
*/
|
|
1051
|
+
const FREE_PRODUCT_UNLOCK_FALLBACK_MS = 5000;
|
|
1052
|
+
/** CP-2171 — how long the cart page fetch for new lines may take before falling back to a reload. */
|
|
1053
|
+
const CART_PAGE_FETCH_TIMEOUT_MS = 8000;
|
|
1054
|
+
/** `sessionStorage` key of the cart page reload guard (see `reloadForMissingCartLines`). */
|
|
1055
|
+
const CART_PAGE_RELOAD_KEY = 'salla-conditional-offer:reloaded-for';
|
|
1056
|
+
/** A theme's cart line forms. Product option forms (`.cart-options`) carry an `item-` id too, but are not lines. */
|
|
1057
|
+
const CART_LINE_FORMS = 'form[id^="item-"]:not(.cart-options form)';
|
|
986
1058
|
/**
|
|
987
1059
|
* Clear space kept between the ⓘ popover and the viewport edge when it is nudged back
|
|
988
1060
|
* inside. Purely cosmetic — one gutter, both edges, both directions.
|
|
@@ -1015,10 +1087,32 @@ const EN_FALLBACKS = {
|
|
|
1015
1087
|
details_title: 'Offer details',
|
|
1016
1088
|
free_gift: 'Free Gift',
|
|
1017
1089
|
free_products_title: 'Free products selected from the offer',
|
|
1090
|
+
// CP-5033 — the gift tier is reached but none of its gifts can be added.
|
|
1091
|
+
free_products_unavailable: 'The free products are currently unavailable',
|
|
1092
|
+
// CP-2171 — the free product picker.
|
|
1093
|
+
free_products_choose: 'Choose :count free products',
|
|
1094
|
+
// Its own key, not :count = 2: Arabic uses the dual form, with no number.
|
|
1095
|
+
free_products_choose_two: 'Choose 2 free products',
|
|
1096
|
+
free_product_choose_one: 'Choose your free product',
|
|
1097
|
+
// The banner line at the gift tier: "…Choose it/them now" while no gift is picked yet, then
|
|
1098
|
+
// the plain line once one is. Same one / two / :count split as the picker title.
|
|
1099
|
+
free_product_got_one: 'You got a free product',
|
|
1100
|
+
free_product_got_pick_one: 'You got a free product! Choose it now',
|
|
1101
|
+
free_products_got: 'You got :count free products',
|
|
1102
|
+
free_products_got_pick: 'You got :count free products! Choose them now',
|
|
1103
|
+
free_products_got_pick_two: 'You got 2 free products! Choose them now',
|
|
1104
|
+
free_products_got_two: 'You got 2 free products',
|
|
1105
|
+
// A picker tier's reward in "Add :remaining and get :reward" and the tooltip rows, instead of
|
|
1106
|
+
// the server's count-less "Free Gift". Same one / two / :count split.
|
|
1107
|
+
free_product_reward_one: 'a free product',
|
|
1108
|
+
free_products_reward: ':count free products',
|
|
1109
|
+
free_products_reward_two: '2 free products',
|
|
1018
1110
|
guidance_conditions_unmet: 'Your cart doesn’t meet the conditions for the next reward yet',
|
|
1019
1111
|
guidance_next: 'Add :remaining and get :reward',
|
|
1020
1112
|
guidance_next_generic: 'Add :remaining to unlock the next reward',
|
|
1021
1113
|
manage_offer: 'Manage offer',
|
|
1114
|
+
// The picker's toggle. Its own key, so offers without a picker keep master's "Manage offer".
|
|
1115
|
+
offer_toggle: 'Offer',
|
|
1022
1116
|
tier_requirement: 'Spend :threshold and get :reward',
|
|
1023
1117
|
tier_requirement_count: 'Buy :threshold and get :reward',
|
|
1024
1118
|
tier_requirement_count_generic: 'Buy :threshold to unlock this offer',
|
|
@@ -1028,6 +1122,15 @@ const EN_FALLBACKS = {
|
|
|
1028
1122
|
/** The gift price label reuses the platform's existing "free of charge" string. */
|
|
1029
1123
|
const FREE_LABEL_KEY = 'pages.checkout.free_charge';
|
|
1030
1124
|
const FREE_LABEL_FALLBACK = 'Free';
|
|
1125
|
+
/**
|
|
1126
|
+
* CP-2171 — the picker's add label is the platform's shared "Add", not a cart-offer key of its
|
|
1127
|
+
* own: the dictionaries keep one string per meaning.
|
|
1128
|
+
*/
|
|
1129
|
+
const ADD_LABEL_KEY = 'common.elements.add';
|
|
1130
|
+
const ADD_LABEL_FALLBACK = 'Add';
|
|
1131
|
+
// The "Free products" panel title reuses the loyalty program's existing "Free products".
|
|
1132
|
+
const FREE_PRODUCTS_TITLE_KEY = 'pages.loyalty_program.free_product_title';
|
|
1133
|
+
const FREE_PRODUCTS_TITLE_FALLBACK = 'Free products';
|
|
1031
1134
|
/**
|
|
1032
1135
|
* Escape a merchant-controlled string before it is substituted into a translation
|
|
1033
1136
|
* template that we then render with `innerHTML`.
|
|
@@ -1135,6 +1238,37 @@ const SallaConditionalOffer$1 = /*@__PURE__*/ proxyCustomElement(class SallaCond
|
|
|
1135
1238
|
this.canRender = true;
|
|
1136
1239
|
/** Free-gift cart LINES belonging to this offer — the "ادارة العرض" card. */
|
|
1137
1240
|
this.gifts = [];
|
|
1241
|
+
/** CP-2171 — every cart line (paid and free), for the picker's per-product state. */
|
|
1242
|
+
this.cartLines = [];
|
|
1243
|
+
/**
|
|
1244
|
+
* CP-2171 — whether `cartLines` came from a cart lookup that answered. Until then the
|
|
1245
|
+
* lines are `[]`: the "Free products" panel and the gift-pick banner line wait for it.
|
|
1246
|
+
*/
|
|
1247
|
+
this.cartLinesLoaded = false;
|
|
1248
|
+
/** CP-2171 — the picker's product cards, keyed by ENCODED product id. */
|
|
1249
|
+
this.freeProductCards = {};
|
|
1250
|
+
/**
|
|
1251
|
+
* CP-2171 — the product (encoded id) an add is in flight for. Held until cart lines read
|
|
1252
|
+
* AFTER the add arrive, not just until the request ends: the cart refresh starts only after
|
|
1253
|
+
* the coalescing window, and a tap in between is ignored, so a double tap adds one unit.
|
|
1254
|
+
*/
|
|
1255
|
+
this.pendingFreeProduct = null;
|
|
1256
|
+
/** `giftsGeneration` when the pending change succeeded; a cart load started after it unlocks. */
|
|
1257
|
+
this.pendingFreeProductSettledAt = null;
|
|
1258
|
+
this.pendingFreeProductTimer = null;
|
|
1259
|
+
/**
|
|
1260
|
+
* `giftsGeneration` when an add from the picker succeeded: the first cart lines read after it
|
|
1261
|
+
* are checked against the page (see `showMissingCartLines`). Kept apart from the
|
|
1262
|
+
* button lock on purpose: the lock's fallback can fire before those lines arrive, and the
|
|
1263
|
+
* page still has to show the new line when they do.
|
|
1264
|
+
*/
|
|
1265
|
+
this.cartPageCheckAfter = null;
|
|
1266
|
+
/** A cart page fetch for missing lines is in flight; lines read meanwhile wait in `pendingCartPageLines`. */
|
|
1267
|
+
this.insertingCartLines = false;
|
|
1268
|
+
this.pendingCartPageLines = null;
|
|
1269
|
+
/** Re-reads of the cart after a failed lookup while a picker change waits; see `retryPickerCartLookup`. */
|
|
1270
|
+
this.pickerLookupRetries = 0;
|
|
1271
|
+
this.pickerLookupTimer = null;
|
|
1138
1272
|
/**
|
|
1139
1273
|
* How many LINES the cart holds — `null` while that is genuinely UNKNOWN.
|
|
1140
1274
|
*
|
|
@@ -1400,6 +1534,9 @@ const SallaConditionalOffer$1 = /*@__PURE__*/ proxyCustomElement(class SallaCond
|
|
|
1400
1534
|
clearTimeout(this.cartRefreshTimer);
|
|
1401
1535
|
this.cartRefreshTimer = null;
|
|
1402
1536
|
}
|
|
1537
|
+
// A dead host has no picker to unlock; drop the fallback timer with the rest.
|
|
1538
|
+
this.unlockFreeProduct();
|
|
1539
|
+
this.clearPickerLookupRetry();
|
|
1403
1540
|
// Likewise: the timer goes, `cartLookupRetryPending` stays.
|
|
1404
1541
|
if (this.cartLookupRetryTimer !== null) {
|
|
1405
1542
|
clearTimeout(this.cartLookupRetryTimer);
|
|
@@ -1889,6 +2026,8 @@ const SallaConditionalOffer$1 = /*@__PURE__*/ proxyCustomElement(class SallaCond
|
|
|
1889
2026
|
return;
|
|
1890
2027
|
this.cartRefreshPending = false;
|
|
1891
2028
|
this.refresh();
|
|
2029
|
+
// A new cart change starts a new picker retry episode: this pass asks again itself.
|
|
2030
|
+
this.clearPickerLookupRetry();
|
|
1892
2031
|
refreshCartFreeGifts().then(() => this.loadCart(), () => this.loadCart());
|
|
1893
2032
|
}
|
|
1894
2033
|
/**
|
|
@@ -1955,6 +2094,14 @@ const SallaConditionalOffer$1 = /*@__PURE__*/ proxyCustomElement(class SallaCond
|
|
|
1955
2094
|
const text = salla.lang.get(FREE_LABEL_KEY);
|
|
1956
2095
|
return text && text !== FREE_LABEL_KEY ? text : FREE_LABEL_FALLBACK;
|
|
1957
2096
|
}
|
|
2097
|
+
getAddLabel() {
|
|
2098
|
+
return this.sharedLabel(ADD_LABEL_KEY, ADD_LABEL_FALLBACK);
|
|
2099
|
+
}
|
|
2100
|
+
/** A platform-wide label (not `blocks.cart_offer.*`): its own text, or `fallback` when missing. */
|
|
2101
|
+
sharedLabel(key, fallback) {
|
|
2102
|
+
const text = salla.lang.get(key);
|
|
2103
|
+
return text && text !== key ? text : fallback;
|
|
2104
|
+
}
|
|
1958
2105
|
/**
|
|
1959
2106
|
* Read a `blocks.cart_offer.*` translation.
|
|
1960
2107
|
*
|
|
@@ -2084,6 +2231,7 @@ const SallaConditionalOffer$1 = /*@__PURE__*/ proxyCustomElement(class SallaCond
|
|
|
2084
2231
|
// Re-asked now that `this.offer` is known, because the gift filter is keyed to
|
|
2085
2232
|
// the offer id — the early pass in `load()` could only answer the COUNT.
|
|
2086
2233
|
this.loadCart();
|
|
2234
|
+
this.loadFreeProductCards();
|
|
2087
2235
|
}
|
|
2088
2236
|
catch (error) {
|
|
2089
2237
|
if (this.isSuperseded(generation))
|
|
@@ -2148,8 +2296,8 @@ const SallaConditionalOffer$1 = /*@__PURE__*/ proxyCustomElement(class SallaCond
|
|
|
2148
2296
|
*/
|
|
2149
2297
|
loadCart() {
|
|
2150
2298
|
const generation = ++this.giftsGeneration;
|
|
2151
|
-
return Promise.all([getCartFreeGiftItems(), getCartItemCount()])
|
|
2152
|
-
.then(([items, count]) => {
|
|
2299
|
+
return Promise.all([getCartFreeGiftItems(), getCartItemCount(), getCartProductLines()])
|
|
2300
|
+
.then(([items, count, lines]) => {
|
|
2153
2301
|
if (generation !== this.giftsGeneration || this.isDisconnected)
|
|
2154
2302
|
return;
|
|
2155
2303
|
/**
|
|
@@ -2163,6 +2311,8 @@ const SallaConditionalOffer$1 = /*@__PURE__*/ proxyCustomElement(class SallaCond
|
|
|
2163
2311
|
*/
|
|
2164
2312
|
if (this.cartRefreshPending)
|
|
2165
2313
|
return;
|
|
2314
|
+
// Was this pass the cart, or a failed lookup's fallback? See below.
|
|
2315
|
+
const answered = hasAuthoritativeCartData();
|
|
2166
2316
|
/**
|
|
2167
2317
|
* NEVER walks a KNOWN count back to `null`. `count` is `null` whenever the
|
|
2168
2318
|
* shared lookup could not answer — which, on the removal path, is every
|
|
@@ -2199,6 +2349,28 @@ const SallaConditionalOffer$1 = /*@__PURE__*/ proxyCustomElement(class SallaCond
|
|
|
2199
2349
|
this.deferredByEmptyCart = false;
|
|
2200
2350
|
this.refresh();
|
|
2201
2351
|
}
|
|
2352
|
+
/**
|
|
2353
|
+
* A failed lookup hands back the cached lines, or `[]` right after a cart
|
|
2354
|
+
* update — not the cart. Publishing them would show an added gift as Add
|
|
2355
|
+
* again (one more tap is a PAID unit) and end the post-change checks below
|
|
2356
|
+
* as though fresh lines had arrived. Keep what we had, and while a picker
|
|
2357
|
+
* change is waiting on real lines — or a shown picker has never had any, so
|
|
2358
|
+
* Add is still disabled — ask again: the count alone schedules no retry once
|
|
2359
|
+
* the cart event has supplied one. A cart with no items (no banner) asks
|
|
2360
|
+
* nothing more.
|
|
2361
|
+
*
|
|
2362
|
+
* An offer without a picker has none of this at stake and goes on as it always
|
|
2363
|
+
* did: its gift card must still follow a removed gift, and nothing would retry.
|
|
2364
|
+
*/
|
|
2365
|
+
const hasPicker = this.getPickerStage(this.getStages()) !== null;
|
|
2366
|
+
const awaitsPickerChange = this.pendingFreeProductSettledAt !== null || this.cartPageCheckAfter !== null;
|
|
2367
|
+
if (!answered && (hasPicker || awaitsPickerChange)) {
|
|
2368
|
+
if (awaitsPickerChange || (!this.cartLinesLoaded && this.hasCartItems())) {
|
|
2369
|
+
this.retryPickerCartLookup();
|
|
2370
|
+
}
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
this.clearPickerLookupRetry();
|
|
2202
2374
|
/**
|
|
2203
2375
|
* The gift card is keyed to the offer, so it can only be filled once the
|
|
2204
2376
|
* offer is known. The early pass fired from `load()` runs BEFORE
|
|
@@ -2208,6 +2380,16 @@ const SallaConditionalOffer$1 = /*@__PURE__*/ proxyCustomElement(class SallaCond
|
|
|
2208
2380
|
* one.
|
|
2209
2381
|
*/
|
|
2210
2382
|
this.gifts = this.offer ? this.selectOwnGifts(items) : [];
|
|
2383
|
+
this.cartLines = this.offer ? lines : [];
|
|
2384
|
+
this.cartLinesLoaded = !!this.offer;
|
|
2385
|
+
// These lines were read after the pending change landed: the picker may act again.
|
|
2386
|
+
if (this.pendingFreeProductSettledAt !== null && generation > this.pendingFreeProductSettledAt) {
|
|
2387
|
+
this.unlockFreeProduct();
|
|
2388
|
+
}
|
|
2389
|
+
if (this.cartPageCheckAfter !== null && generation > this.cartPageCheckAfter) {
|
|
2390
|
+
this.cartPageCheckAfter = null;
|
|
2391
|
+
this.showMissingCartLines(lines);
|
|
2392
|
+
}
|
|
2211
2393
|
})
|
|
2212
2394
|
.catch(error => {
|
|
2213
2395
|
this.safeLog('cart lookup failed', error);
|
|
@@ -2239,6 +2421,27 @@ const SallaConditionalOffer$1 = /*@__PURE__*/ proxyCustomElement(class SallaCond
|
|
|
2239
2421
|
this.loadCart();
|
|
2240
2422
|
}, CART_LOOKUP_RETRY_DELAYS_MS[this.cartLookupRetries]);
|
|
2241
2423
|
}
|
|
2424
|
+
/**
|
|
2425
|
+
* Ask for the cart again after a lookup that could not answer while a picker change is
|
|
2426
|
+
* waiting on real lines. Its own bounded episode, on the count retry's delays: that one
|
|
2427
|
+
* ends as soon as the cart event has supplied a count, which is exactly when this is needed.
|
|
2428
|
+
*/
|
|
2429
|
+
retryPickerCartLookup() {
|
|
2430
|
+
if (this.pickerLookupTimer !== null || this.pickerLookupRetries >= CART_LOOKUP_RETRY_DELAYS_MS.length)
|
|
2431
|
+
return;
|
|
2432
|
+
this.pickerLookupTimer = setTimeout(() => {
|
|
2433
|
+
this.pickerLookupTimer = null;
|
|
2434
|
+
this.pickerLookupRetries++;
|
|
2435
|
+
this.loadCart();
|
|
2436
|
+
}, CART_LOOKUP_RETRY_DELAYS_MS[this.pickerLookupRetries]);
|
|
2437
|
+
}
|
|
2438
|
+
clearPickerLookupRetry() {
|
|
2439
|
+
this.pickerLookupRetries = 0;
|
|
2440
|
+
if (this.pickerLookupTimer === null)
|
|
2441
|
+
return;
|
|
2442
|
+
clearTimeout(this.pickerLookupTimer);
|
|
2443
|
+
this.pickerLookupTimer = null;
|
|
2444
|
+
}
|
|
2242
2445
|
/**
|
|
2243
2446
|
* End the retry episode: a lookup answered, or the cart changed under it. The count
|
|
2244
2447
|
* goes back to zero so a LATER failure gets its own full set of attempts.
|
|
@@ -2295,7 +2498,7 @@ const SallaConditionalOffer$1 = /*@__PURE__*/ proxyCustomElement(class SallaCond
|
|
|
2295
2498
|
* an older backend — is measured by the offer-level value exactly as before.
|
|
2296
2499
|
*/
|
|
2297
2500
|
getProgress(stages) {
|
|
2298
|
-
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));
|
|
2501
|
+
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));
|
|
2299
2502
|
}
|
|
2300
2503
|
/**
|
|
2301
2504
|
* A tier's render key. `id` is the stable identity across refetches; the array
|
|
@@ -2352,6 +2555,15 @@ const SallaConditionalOffer$1 = /*@__PURE__*/ proxyCustomElement(class SallaCond
|
|
|
2352
2555
|
* why every caller has a reward-less sentence to fall back to.
|
|
2353
2556
|
*/
|
|
2354
2557
|
getRewardText(discount) {
|
|
2558
|
+
// CP-2171 — Figma: "Add 140 and get 2 free products". The server's label has no count.
|
|
2559
|
+
if (this.isPickerTier(discount)) {
|
|
2560
|
+
const limit = Math.max(1, Math.floor(Number(discount.max_free_products) || 1));
|
|
2561
|
+
if (limit === 1)
|
|
2562
|
+
return this.trans('free_product_reward_one');
|
|
2563
|
+
if (limit === 2)
|
|
2564
|
+
return this.trans('free_products_reward_two');
|
|
2565
|
+
return this.trans('free_products_reward', { count: `${limit}` });
|
|
2566
|
+
}
|
|
2355
2567
|
return (discount?.reward_label ?? '').trim();
|
|
2356
2568
|
}
|
|
2357
2569
|
/**
|
|
@@ -2373,7 +2585,13 @@ const SallaConditionalOffer$1 = /*@__PURE__*/ proxyCustomElement(class SallaCond
|
|
|
2373
2585
|
getGuidanceMessage(stages, progress) {
|
|
2374
2586
|
if (progress.nextUnmetIndex === NO_STAGE_REACHED)
|
|
2375
2587
|
return '';
|
|
2376
|
-
|
|
2588
|
+
// CP-2171 — the engine's own gap for a picker tier, when the server sends it: `current_value`
|
|
2589
|
+
// counts the gift units the tier would free, so it can read as past the threshold while the
|
|
2590
|
+
// tier is still short ("conditions unmet" with no amount).
|
|
2591
|
+
const engineAmountNeeded = Number(stages[progress.nextUnmetIndex]?.amount_needed);
|
|
2592
|
+
const remainingValue = this.isPickerTier(stages[progress.nextUnmetIndex]) && Number.isFinite(engineAmountNeeded) && engineAmountNeeded > 0
|
|
2593
|
+
? engineAmountNeeded
|
|
2594
|
+
: getRemainingToStage(progress.nextUnmetIndex, progress, this.stageMeasure());
|
|
2377
2595
|
if (remainingValue <= 0)
|
|
2378
2596
|
return this.trans('guidance_conditions_unmet');
|
|
2379
2597
|
const remaining = this.formatRequirement(remainingValue);
|
|
@@ -2651,26 +2869,403 @@ const SallaConditionalOffer$1 = /*@__PURE__*/ proxyCustomElement(class SallaCond
|
|
|
2651
2869
|
}, innerHTML: this.getTierSentence(discount, index, progress) }))));
|
|
2652
2870
|
}))));
|
|
2653
2871
|
}
|
|
2872
|
+
/**
|
|
2873
|
+
* CP-2171 — the tier the free product picker belongs to: the first `free_product` tier that
|
|
2874
|
+
* carries a non-empty `free_products` list. `null` on an offer without one, which keeps the
|
|
2875
|
+
* read-only gift card exactly as before. An empty list (every listed product unavailable) is
|
|
2876
|
+
* treated as no picker: there is nothing to choose, so a "Choose N" header would be a dead end.
|
|
2877
|
+
*/
|
|
2878
|
+
getPickerStage(stages) {
|
|
2879
|
+
return stages.find(stage => this.isPickerTier(stage)) ?? null;
|
|
2880
|
+
}
|
|
2881
|
+
isPickerTier(stage) {
|
|
2882
|
+
return stage?.type === DiscountType.FREE_PRODUCT && Array.isArray(stage.free_products) && stage.free_products.length > 0;
|
|
2883
|
+
}
|
|
2884
|
+
/**
|
|
2885
|
+
* CP-5033 — the furthest tier the cart's value reaches is a gift tier with none of its gifts
|
|
2886
|
+
* addable (the server lists only what can be added: out of stock, hidden and deleted are left
|
|
2887
|
+
* out), and the cart holds none of this offer's gifts. The engine pays nothing for that tier,
|
|
2888
|
+
* so neither "congrats" nor "conditions unmet" is true.
|
|
2889
|
+
*/
|
|
2890
|
+
areGiftsUnavailable(stages, progress) {
|
|
2891
|
+
const stage = stages[progress.lastReachedIndex];
|
|
2892
|
+
return stage?.type === DiscountType.FREE_PRODUCT
|
|
2893
|
+
&& Array.isArray(stage.free_products) && stage.free_products.length === 0
|
|
2894
|
+
&& this.gifts.length === 0;
|
|
2895
|
+
}
|
|
2896
|
+
/**
|
|
2897
|
+
* Load the picker's product cards (name, image, price) — one `salla.product.fetch` for
|
|
2898
|
+
* the ids not loaded yet. Never rejects: a failed lookup leaves the cards out, not the banner.
|
|
2899
|
+
*/
|
|
2900
|
+
loadFreeProductCards() {
|
|
2901
|
+
const stage = this.getPickerStage(this.getStages());
|
|
2902
|
+
const ids = (stage?.free_products ?? [])
|
|
2903
|
+
.map(ref => String(ref.id))
|
|
2904
|
+
.filter(id => id && !this.freeProductCards[id]);
|
|
2905
|
+
if (!ids.length)
|
|
2906
|
+
return Promise.resolve();
|
|
2907
|
+
const listedIds = new Set((stage.free_products ?? []).map(ref => String(ref.id)));
|
|
2908
|
+
return Promise.resolve()
|
|
2909
|
+
.then(() => salla.product.fetch({ source: 'selected', source_value: ids }))
|
|
2910
|
+
.then((response) => {
|
|
2911
|
+
if (this.isDisconnected)
|
|
2912
|
+
return;
|
|
2913
|
+
const cards = { ...this.freeProductCards };
|
|
2914
|
+
(Array.isArray(response?.data) ? response.data : []).forEach((product) => {
|
|
2915
|
+
const id = String(product?.id ?? '');
|
|
2916
|
+
if (!id || !listedIds.has(id))
|
|
2917
|
+
return;
|
|
2918
|
+
cards[id] = {
|
|
2919
|
+
id,
|
|
2920
|
+
name: String(product.name ?? ''),
|
|
2921
|
+
imageUrl: String(product.image?.url ?? ''),
|
|
2922
|
+
imageAlt: String(product.image?.alt ?? product.name ?? ''),
|
|
2923
|
+
price: Number.isFinite(Number(product.price)) ? Number(product.price) : null,
|
|
2924
|
+
};
|
|
2925
|
+
});
|
|
2926
|
+
this.freeProductCards = cards;
|
|
2927
|
+
})
|
|
2928
|
+
.catch(error => this.safeLog('free product lookup failed', error));
|
|
2929
|
+
}
|
|
2930
|
+
/**
|
|
2931
|
+
* The picker's view of the cart for this offer.
|
|
2932
|
+
*
|
|
2933
|
+
* - `freeCount`: products a line of THIS offer made free — the engine's own verdict, read
|
|
2934
|
+
* off the cart lines (`added_by_offer` + `offer_id`), never re-derived here. Gifts of other
|
|
2935
|
+
* offers (another cart offer, Buy X Get Y) are not counted against this offer's limit.
|
|
2936
|
+
* - `reached`: the tier's threshold is met. Read from `current_value` against `min_spend`,
|
|
2937
|
+
* NOT from `is_applied`: a free product tier is only "applied" once a listed product is
|
|
2938
|
+
* in the cart, so gating "Add" on it would never let the customer add the first one.
|
|
2939
|
+
*/
|
|
2940
|
+
getPickerState(stage) {
|
|
2941
|
+
const allFreeLines = this.cartLines.filter(line => line.freeQuantity > 0);
|
|
2942
|
+
const freeLines = this.selectOwnFreeLines(allFreeLines);
|
|
2943
|
+
const free = new Set(freeLines.map(line => line.productId).filter(id => id !== null));
|
|
2944
|
+
const limit = Math.max(1, Math.floor(Number(stage.max_free_products) || 1));
|
|
2945
|
+
// The engine's answer when the server sends it; `current_value >= min_spend` is only a
|
|
2946
|
+
// fallback — it is gross of earlier discounts, so it can say "choose" while the engine
|
|
2947
|
+
// would still charge for the product.
|
|
2948
|
+
const value = Number(stage.current_value ?? this.offer?.details?.current_value ?? 0);
|
|
2949
|
+
const reached = typeof stage.can_add_free === 'boolean'
|
|
2950
|
+
? stage.can_add_free
|
|
2951
|
+
: Number.isFinite(value) && value >= Number(stage.min_spend);
|
|
2952
|
+
return { limit, reached, freeCount: free.size };
|
|
2953
|
+
}
|
|
2954
|
+
/**
|
|
2955
|
+
* The free lines this offer granted — the SAME rule as `selectOwnGifts`, so the picker and
|
|
2956
|
+
* the gifts card never disagree: lines matching the offer id; only when none match, the
|
|
2957
|
+
* lines whose `offer_id` is unreadable.
|
|
2958
|
+
*/
|
|
2959
|
+
selectOwnFreeLines(lines) {
|
|
2960
|
+
const offerId = Number(this.offer?.id);
|
|
2961
|
+
if (!Number.isFinite(offerId))
|
|
2962
|
+
return lines;
|
|
2963
|
+
const owned = lines.filter(line => line.offerId !== null && Number(line.offerId) === offerId);
|
|
2964
|
+
return owned.length ? owned : lines.filter(line => line.offerId === null);
|
|
2965
|
+
}
|
|
2966
|
+
/**
|
|
2967
|
+
* Add a free product to the cart — always from the cart page, never a redirect.
|
|
2968
|
+
*
|
|
2969
|
+
* Quick-add, exactly like the single free product was auto-added before CP-2171: a variant
|
|
2970
|
+
* (SKU) product gets its default SKU — `is_default` first, or the first in stock when stock
|
|
2971
|
+
* is per SKU (`AddToCart::addItemToCart()`) — with that SKU's option values. Any listed
|
|
2972
|
+
* product in the cart counts, so the engine then frees it. A failed add is left to the SDK,
|
|
2973
|
+
* which already shows the shopper its error notice.
|
|
2974
|
+
*/
|
|
2975
|
+
addFreeProduct(card) {
|
|
2976
|
+
if (this.pendingFreeProduct)
|
|
2977
|
+
return;
|
|
2978
|
+
this.pendingFreeProduct = card.id;
|
|
2979
|
+
this.runFreeProductChange(() => Promise.resolve(salla.cart.addItem(card.id)).then(() => { this.cartPageCheckAfter = this.giftsGeneration; }), 'free product add failed');
|
|
2980
|
+
}
|
|
2981
|
+
/** Its own method so specs can observe it; the mock window cannot reload. */
|
|
2982
|
+
reloadPage() {
|
|
2983
|
+
window.location.reload();
|
|
2984
|
+
}
|
|
2985
|
+
/**
|
|
2986
|
+
* Its own method so specs can stub it; the mock window has no `fetch`. Aborted after
|
|
2987
|
+
* `CART_PAGE_FETCH_TIMEOUT_MS`: a fetch that never settles would hold `insertingCartLines`
|
|
2988
|
+
* for the page view, and every later line would only be queued behind it.
|
|
2989
|
+
*/
|
|
2990
|
+
fetchCartPageHtml() {
|
|
2991
|
+
const controller = new AbortController();
|
|
2992
|
+
const timer = setTimeout(() => controller.abort(), CART_PAGE_FETCH_TIMEOUT_MS);
|
|
2993
|
+
// `no-store`: a cached copy of the page would lack the new line and force the reload.
|
|
2994
|
+
return fetch(window.location.href, { cache: 'no-store', credentials: 'same-origin', headers: { Accept: 'text/html' }, signal: controller.signal })
|
|
2995
|
+
.then(response => (response.ok ? response.text() : Promise.reject(new Error(`cart page ${response.status}`))))
|
|
2996
|
+
.finally(() => clearTimeout(timer));
|
|
2997
|
+
}
|
|
2998
|
+
/**
|
|
2999
|
+
* Show the cart lines an add from the picker created that the page has no `form#item-<id>`
|
|
3000
|
+
* for. Themes render cart lines on the server and only refresh lines already on the page, so
|
|
3001
|
+
* the new line (and its price) would otherwise appear only after a manual refresh.
|
|
3002
|
+
*
|
|
3003
|
+
* The cart page is fetched again and the missing line forms are inserted, in the server's
|
|
3004
|
+
* order — no reload. The fetched markup is fresh, so its prices need no update. Its inline
|
|
3005
|
+
* handlers and web components work as on a loaded page; only what the theme's own script wires
|
|
3006
|
+
* to a line at page load (e.g. raed's loading overlay on a quantity change) is missing.
|
|
3007
|
+
* Any failure (fetch, a line the fetched page lacks) falls back to `reloadForMissingCartLines`.
|
|
3008
|
+
*
|
|
3009
|
+
* Left alone on a page with no cart line forms at all (not a cart page this can reason about).
|
|
3010
|
+
* One fetch at a time; lines read meanwhile are checked again after it.
|
|
3011
|
+
*/
|
|
3012
|
+
showMissingCartLines(lines) {
|
|
3013
|
+
if (this.insertingCartLines) {
|
|
3014
|
+
this.pendingCartPageLines = lines;
|
|
3015
|
+
return;
|
|
3016
|
+
}
|
|
3017
|
+
const missing = this.missingCartLineIds(lines);
|
|
3018
|
+
if (!missing.length)
|
|
3019
|
+
return;
|
|
3020
|
+
this.insertingCartLines = true;
|
|
3021
|
+
// Started inside the chain, so a synchronous throw (no `fetch`) still reaches `catch` and `finally`.
|
|
3022
|
+
Promise.resolve()
|
|
3023
|
+
.then(() => this.fetchCartPageHtml())
|
|
3024
|
+
.then(html => {
|
|
3025
|
+
if (this.isDisconnected || this.cartChangedDuringPageFetch())
|
|
3026
|
+
return;
|
|
3027
|
+
const stillMissing = this.missingCartLineIds(this.cartLines);
|
|
3028
|
+
if (stillMissing.length && !this.insertCartLineForms(html, stillMissing)) {
|
|
3029
|
+
this.reloadForMissingCartLines(stillMissing);
|
|
3030
|
+
}
|
|
3031
|
+
})
|
|
3032
|
+
.catch(error => {
|
|
3033
|
+
this.safeLog('cart page fetch for the new lines failed, reloading', error);
|
|
3034
|
+
if (this.isDisconnected || this.cartChangedDuringPageFetch())
|
|
3035
|
+
return;
|
|
3036
|
+
this.reloadForMissingCartLines(this.missingCartLineIds(this.cartLines));
|
|
3037
|
+
})
|
|
3038
|
+
.finally(() => {
|
|
3039
|
+
this.insertingCartLines = false;
|
|
3040
|
+
const pending = this.pendingCartPageLines;
|
|
3041
|
+
this.pendingCartPageLines = null;
|
|
3042
|
+
if (pending && !this.isDisconnected)
|
|
3043
|
+
this.showMissingCartLines(pending);
|
|
3044
|
+
});
|
|
3045
|
+
}
|
|
3046
|
+
/**
|
|
3047
|
+
* A picker change started while the cart page was fetched (e.g. a second add),
|
|
3048
|
+
* so neither the fetched page nor the cart lines can be trusted: inserting now could bring
|
|
3049
|
+
* back a line the cart no longer has. The answer is dropped and the cart read after that
|
|
3050
|
+
* change checks the page again (the change itself arms `cartPageCheckAfter` when it lands;
|
|
3051
|
+
* armed here too, for a change that fails and so arms nothing).
|
|
3052
|
+
*
|
|
3053
|
+
* Lines are always read from `cartLines` once the page arrives, never from the call's own
|
|
3054
|
+
* `lines`: a change that landed during the fetch has already replaced them there.
|
|
3055
|
+
*/
|
|
3056
|
+
cartChangedDuringPageFetch() {
|
|
3057
|
+
// Lines not read since a change (the lock timed out on them) are no basis either.
|
|
3058
|
+
if (this.pendingFreeProduct === null && this.cartLinesLoaded)
|
|
3059
|
+
return false;
|
|
3060
|
+
if (this.cartPageCheckAfter === null)
|
|
3061
|
+
this.cartPageCheckAfter = this.giftsGeneration;
|
|
3062
|
+
return true;
|
|
3063
|
+
}
|
|
3064
|
+
/** Ids of `lines` the page has no form for, sorted; empty on a page with no cart line forms. */
|
|
3065
|
+
missingCartLineIds(lines) {
|
|
3066
|
+
try {
|
|
3067
|
+
if (!document.querySelector(CART_LINE_FORMS))
|
|
3068
|
+
return [];
|
|
3069
|
+
return lines.map(line => line.id).filter(id => !document.getElementById(`item-${id}`)).sort();
|
|
3070
|
+
}
|
|
3071
|
+
catch (error) {
|
|
3072
|
+
this.safeLog('cart page check after a free product add failed', error);
|
|
3073
|
+
return [];
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
/**
|
|
3077
|
+
* Insert the `missing` line forms from the fetched cart page `html`, each right before the next
|
|
3078
|
+
* line the page already has (in the fetched order), else after the last one. Returns false —
|
|
3079
|
+
* nothing inserted — when the fetched page lacks any of them.
|
|
3080
|
+
*/
|
|
3081
|
+
insertCartLineForms(html, missing) {
|
|
3082
|
+
try {
|
|
3083
|
+
const fetched = Array.from(new DOMParser().parseFromString(html, 'text/html').querySelectorAll(CART_LINE_FORMS));
|
|
3084
|
+
// `getAttribute`, not `form.id`: a line form holds an `<input name="id">`, which shadows the property.
|
|
3085
|
+
const fetchedIds = fetched.map(form => form.getAttribute('id'));
|
|
3086
|
+
if (missing.some(id => !fetchedIds.includes(`item-${id}`)))
|
|
3087
|
+
return false;
|
|
3088
|
+
fetched.forEach((form, index) => {
|
|
3089
|
+
const id = fetchedIds[index];
|
|
3090
|
+
if (!missing.includes(id.slice('item-'.length)) || document.getElementById(id))
|
|
3091
|
+
return;
|
|
3092
|
+
// Moved, not cloned: inserting adopts it into this document, and its web components
|
|
3093
|
+
// upgrade once connected. Its inline handlers (`onchange`, delete `onclick`) come along.
|
|
3094
|
+
const next = fetchedIds.slice(index + 1).map(nextId => document.getElementById(nextId)).find(Boolean);
|
|
3095
|
+
if (next) {
|
|
3096
|
+
next.parentNode.insertBefore(form, next);
|
|
3097
|
+
}
|
|
3098
|
+
else {
|
|
3099
|
+
const onPage = document.querySelectorAll(CART_LINE_FORMS);
|
|
3100
|
+
const last = onPage[onPage.length - 1];
|
|
3101
|
+
last.parentNode.insertBefore(form, last.nextSibling);
|
|
3102
|
+
}
|
|
3103
|
+
});
|
|
3104
|
+
return missing.every(id => document.getElementById(`item-${id}`));
|
|
3105
|
+
}
|
|
3106
|
+
catch (error) {
|
|
3107
|
+
this.safeLog('cart line insert failed', error);
|
|
3108
|
+
return false;
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
/**
|
|
3112
|
+
* Reload once, when the missing lines could not be inserted. Never twice for the same missing
|
|
3113
|
+
* lines, so a line a theme never renders cannot loop the page. The guard lives in
|
|
3114
|
+
* `sessionStorage`; where storage is blocked the page still reloads, but never twice in a row.
|
|
3115
|
+
*/
|
|
3116
|
+
reloadForMissingCartLines(missing) {
|
|
3117
|
+
if (!missing.length)
|
|
3118
|
+
return;
|
|
3119
|
+
const signature = missing.join(',');
|
|
3120
|
+
const stored = this.readReloadGuard();
|
|
3121
|
+
if (stored === signature)
|
|
3122
|
+
return;
|
|
3123
|
+
if (stored === undefined && this.pageWasReloaded())
|
|
3124
|
+
return;
|
|
3125
|
+
this.writeReloadGuard(signature);
|
|
3126
|
+
this.reloadPage();
|
|
3127
|
+
}
|
|
3128
|
+
/** The reload guard, or `undefined` when storage cannot be read (blocked, disabled). */
|
|
3129
|
+
readReloadGuard() {
|
|
3130
|
+
try {
|
|
3131
|
+
return window.sessionStorage.getItem(CART_PAGE_RELOAD_KEY);
|
|
3132
|
+
}
|
|
3133
|
+
catch {
|
|
3134
|
+
return undefined;
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
writeReloadGuard(signature) {
|
|
3138
|
+
try {
|
|
3139
|
+
window.sessionStorage.setItem(CART_PAGE_RELOAD_KEY, signature);
|
|
3140
|
+
}
|
|
3141
|
+
catch {
|
|
3142
|
+
// Storage full or blocked: pageWasReloaded() is the guard on the next load.
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
/** Whether this page load is itself a reload — the guard when storage is unavailable. */
|
|
3146
|
+
pageWasReloaded() {
|
|
3147
|
+
try {
|
|
3148
|
+
const navigation = window.performance?.getEntriesByType?.('navigation')?.[0];
|
|
3149
|
+
return navigation?.type === 'reload';
|
|
3150
|
+
}
|
|
3151
|
+
catch {
|
|
3152
|
+
return false;
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
/**
|
|
3156
|
+
* Run one add. A failure unlocks at once — nothing changed. A success stays locked
|
|
3157
|
+
* until a cart load that started after it publishes (see `loadCart`), or the fallback fires.
|
|
3158
|
+
*/
|
|
3159
|
+
runFreeProductChange(change, failure) {
|
|
3160
|
+
Promise.resolve()
|
|
3161
|
+
.then(change)
|
|
3162
|
+
.then(() => {
|
|
3163
|
+
if (this.isDisconnected)
|
|
3164
|
+
return this.unlockFreeProduct();
|
|
3165
|
+
this.pendingFreeProductSettledAt = this.giftsGeneration;
|
|
3166
|
+
this.pendingFreeProductTimer = setTimeout(() => this.unlockOnStaleLines(), FREE_PRODUCT_UNLOCK_FALLBACK_MS);
|
|
3167
|
+
})
|
|
3168
|
+
.catch(error => {
|
|
3169
|
+
this.safeLog(failure, error);
|
|
3170
|
+
this.unlockFreeProduct();
|
|
3171
|
+
// A failed change fires no cart event, so nothing else checks a cart page answer
|
|
3172
|
+
// that was dropped while this change was in flight.
|
|
3173
|
+
if (this.cartPageCheckAfter !== null)
|
|
3174
|
+
this.loadCart();
|
|
3175
|
+
});
|
|
3176
|
+
}
|
|
3177
|
+
/**
|
|
3178
|
+
* The lock's fallback: no cart lines read after the change have arrived, so the lines on
|
|
3179
|
+
* screen are the ones from BEFORE it. Release the lock, mark the lines unread and ask again
|
|
3180
|
+
* on a fresh read — `loadCart` retries within its bound while the picker has no lines.
|
|
3181
|
+
*/
|
|
3182
|
+
unlockOnStaleLines() {
|
|
3183
|
+
this.unlockFreeProduct();
|
|
3184
|
+
this.cartLinesLoaded = false;
|
|
3185
|
+
this.clearPickerLookupRetry();
|
|
3186
|
+
refreshCartFreeGifts().then(() => this.loadCart(), () => this.loadCart());
|
|
3187
|
+
}
|
|
3188
|
+
unlockFreeProduct() {
|
|
3189
|
+
if (this.pendingFreeProductTimer !== null)
|
|
3190
|
+
clearTimeout(this.pendingFreeProductTimer);
|
|
3191
|
+
this.pendingFreeProductTimer = null;
|
|
3192
|
+
this.pendingFreeProductSettledAt = null;
|
|
3193
|
+
this.pendingFreeProduct = null;
|
|
3194
|
+
}
|
|
3195
|
+
/**
|
|
3196
|
+
* CP-2171 — Figma's banner line at the gift tier: "You got N free products", with
|
|
3197
|
+
* "Choose them now" while none is picked yet. N is the tier's limit, not the count picked.
|
|
3198
|
+
*/
|
|
3199
|
+
pickerGotMessage(limit, awaitsPick) {
|
|
3200
|
+
if (limit === 1)
|
|
3201
|
+
return this.trans(awaitsPick ? 'free_product_got_pick_one' : 'free_product_got_one');
|
|
3202
|
+
if (limit === 2)
|
|
3203
|
+
return this.trans(awaitsPick ? 'free_products_got_pick_two' : 'free_products_got_two');
|
|
3204
|
+
return this.trans(awaitsPick ? 'free_products_got_pick' : 'free_products_got', { count: `${limit}` });
|
|
3205
|
+
}
|
|
3206
|
+
getPickerCards(stage) {
|
|
3207
|
+
return (stage.free_products ?? [])
|
|
3208
|
+
.map(ref => this.freeProductCards[String(ref.id)])
|
|
3209
|
+
.filter(Boolean);
|
|
3210
|
+
}
|
|
3211
|
+
/** CP-2171 — Figma "Choose N free products": the counter, then one card per listed product. */
|
|
3212
|
+
renderPicker(stage) {
|
|
3213
|
+
const state = this.getPickerState(stage);
|
|
3214
|
+
const cards = this.getPickerCards(stage);
|
|
3215
|
+
// Cards still loading, or the lookup failed: no header and counter over an empty list.
|
|
3216
|
+
if (!cards.length)
|
|
3217
|
+
return null;
|
|
3218
|
+
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
|
|
3219
|
+
? this.trans('free_product_choose_one')
|
|
3220
|
+
: state.limit === 2
|
|
3221
|
+
? this.trans('free_products_choose_two')
|
|
3222
|
+
: this.trans('free_products_choose', { count: `${state.limit}` }))), h("ul", { class: "s-conditional-offer-picker-list" }, cards.map(card => this.renderPickerCard(card)))));
|
|
3223
|
+
}
|
|
3224
|
+
/**
|
|
3225
|
+
* One picker card: the product, its normal price and Add — the same on every card, whether
|
|
3226
|
+
* the product is in the cart, free or not. Add is never disabled: every tap adds one unit at
|
|
3227
|
+
* the normal price (also once the product is in the cart, and after the free limit is used
|
|
3228
|
+
* up) and the engine decides what is free. What IS free shows in the "Free products" panel
|
|
3229
|
+
* below, never on the card. A tap while the previous add is still in flight is ignored
|
|
3230
|
+
* (`addFreeProduct`), so one double tap is one unit.
|
|
3231
|
+
*
|
|
3232
|
+
* No remove: the picker never deletes a cart line. A free line can also hold paid units, and
|
|
3233
|
+
* deleting it would take what the customer bought with the gift — products leave the cart
|
|
3234
|
+
* from their own cart line only.
|
|
3235
|
+
*/
|
|
3236
|
+
renderPickerCard(card) {
|
|
3237
|
+
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()))));
|
|
3238
|
+
}
|
|
2654
3239
|
/**
|
|
2655
3240
|
* "ادارة العرض". Rendered TWICE with complementary responsive visibility rather than
|
|
2656
3241
|
* moved with `order:`: desktop wants it inline with the banner row, mobile wants a
|
|
2657
3242
|
* full-width bar BELOW the gifts card, and those are different places
|
|
2658
3243
|
* in the flow, not a different order within one flex line.
|
|
2659
3244
|
*/
|
|
2660
|
-
renderGiftsToggle(variant) {
|
|
3245
|
+
renderGiftsToggle(variant, hasPicker) {
|
|
2661
3246
|
return (h("button", { type: "button", id: this.elementId(`toggle-${variant}`), class: {
|
|
2662
3247
|
's-conditional-offer-toggle': true,
|
|
2663
3248
|
's-conditional-offer-toggle-inline': variant === 'inline',
|
|
2664
3249
|
's-conditional-offer-toggle-bar': variant === 'bar',
|
|
2665
|
-
}, "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: {
|
|
3250
|
+
}, "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: {
|
|
2666
3251
|
'sicon-keyboard_arrow_down': true,
|
|
2667
3252
|
's-conditional-offer-toggle-icon': true,
|
|
2668
3253
|
's-conditional-offer-toggle-icon-open': this.isGiftsOpen,
|
|
2669
3254
|
}, "aria-hidden": "true" })));
|
|
2670
3255
|
}
|
|
2671
|
-
/**
|
|
2672
|
-
|
|
2673
|
-
|
|
3256
|
+
/**
|
|
3257
|
+
* Spec §5 — the gifts card: one row per gift line. Read-only on an offer without a picker,
|
|
3258
|
+
* exactly as before; with the CP-2171 picker it becomes the Figma "Free products" panel,
|
|
3259
|
+
* shown only once a gift is in the cart. Its rows are read-only, as are the picker cards: a
|
|
3260
|
+
* gift leaves the cart from its own cart line.
|
|
3261
|
+
*/
|
|
3262
|
+
renderGiftsCard(pickerStage = null) {
|
|
3263
|
+
// No picker: the card exactly as before CP-2171 — no wrapper, so no new box styling.
|
|
3264
|
+
if (!pickerStage) {
|
|
3265
|
+
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)))));
|
|
3266
|
+
}
|
|
3267
|
+
const state = this.getPickerState(pickerStage);
|
|
3268
|
+
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));
|
|
2674
3269
|
}
|
|
2675
3270
|
renderGiftRow(gift) {
|
|
2676
3271
|
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 ? (
|
|
@@ -2717,12 +3312,26 @@ const SallaConditionalOffer$1 = /*@__PURE__*/ proxyCustomElement(class SallaCond
|
|
|
2717
3312
|
const progress = this.getProgress(stages);
|
|
2718
3313
|
const guidance = this.getGuidanceMessage(stages, progress);
|
|
2719
3314
|
const detailsTitle = this.trans('details_title');
|
|
2720
|
-
// Spec §2 — the accordion exists only when there is something to manage
|
|
2721
|
-
|
|
2722
|
-
|
|
3315
|
+
// Spec §2 — the accordion exists only when there is something to manage: gift lines,
|
|
3316
|
+
// or (CP-2171) a free product picker — shown only once the customer has reached its
|
|
3317
|
+
// stage, then before anything is picked too.
|
|
3318
|
+
const pickerStage = this.getPickerStage(stages);
|
|
3319
|
+
const pickerState = pickerStage !== null ? this.getPickerState(pickerStage) : null;
|
|
3320
|
+
const isPickerReached = pickerState?.reached ?? false;
|
|
3321
|
+
const hasGifts = this.gifts.length > 0 || (isPickerReached && this.getPickerCards(pickerStage).length > 0);
|
|
3322
|
+
// The gift tier is reached but none of this offer's gifts is in the cart: the engine
|
|
3323
|
+
// grants that tier only once one is, and pays nothing until then (no fallback to a lower
|
|
3324
|
+
// tier). So neither "congrats" nor "conditions unmet" is true — ask for the pick. Only
|
|
3325
|
+
// once the cart lines are read, or a cart that holds the gift would flash this line.
|
|
3326
|
+
const awaitsGiftPick = isPickerReached && this.cartLinesLoaded && pickerState.freeCount === 0;
|
|
3327
|
+
// "You got N free products" replaces congrats only when the gift tier is the offer's last:
|
|
3328
|
+
// below a higher tier, `guidance` (or congrats for that higher tier) is the truer line.
|
|
3329
|
+
const gotGifts = progress.allMet && isPickerReached && pickerState.freeCount > 0
|
|
3330
|
+
&& stages[stages.length - 1] === pickerStage;
|
|
3331
|
+
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 ? (
|
|
2723
3332
|
// innerHTML: the `:remaining` half carries `salla.money()` markup;
|
|
2724
3333
|
// the reward half is escaped in `getGuidanceMessage`.
|
|
2725
|
-
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));
|
|
3334
|
+
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));
|
|
2726
3335
|
}
|
|
2727
3336
|
get hostEl() { return this; }
|
|
2728
3337
|
static get style() { return sallaConditionalOfferCss; }
|
|
@@ -2731,6 +3340,10 @@ const SallaConditionalOffer$1 = /*@__PURE__*/ proxyCustomElement(class SallaCond
|
|
|
2731
3340
|
"isLoading": [32],
|
|
2732
3341
|
"canRender": [32],
|
|
2733
3342
|
"gifts": [32],
|
|
3343
|
+
"cartLines": [32],
|
|
3344
|
+
"cartLinesLoaded": [32],
|
|
3345
|
+
"freeProductCards": [32],
|
|
3346
|
+
"pendingFreeProduct": [32],
|
|
2734
3347
|
"cartItemCount": [32],
|
|
2735
3348
|
"isTooltipHovered": [32],
|
|
2736
3349
|
"isTooltipPinned": [32],
|