@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*!
|
|
2
2
|
* Crafted with ❤ by Salla
|
|
3
3
|
*/
|
|
4
|
-
import { r as registerInstance, h, H as Host, a as getElement } from './index-
|
|
4
|
+
import { r as registerInstance, h, H as Host, a as getElement } from './index-ze9e0dxM.js';
|
|
5
5
|
|
|
6
6
|
var DiscountType;
|
|
7
7
|
(function (DiscountType) {
|
|
@@ -44,6 +44,9 @@ var OfferType;
|
|
|
44
44
|
* `twilight/packages/twilight/src/api/cart.js:831-846`.
|
|
45
45
|
*/
|
|
46
46
|
const EMPTY_GIFT_ITEMS = Object.freeze([]);
|
|
47
|
+
const EMPTY_PRODUCT_LINES = Object.freeze([]);
|
|
48
|
+
/** Every line from the last parsed response — published in the SAME pass as `itemsCache`. */
|
|
49
|
+
let linesCache = null;
|
|
47
50
|
/**
|
|
48
51
|
* The gift LINES from the last parsed response. `null` is the single "nothing has loaded
|
|
49
52
|
* yet" flag shared with `countCache`, which keeps the two from ever disagreeing about
|
|
@@ -145,6 +148,7 @@ function syncCartIdentity() {
|
|
|
145
148
|
if (cacheIdentity !== undefined && cacheIdentity !== current) {
|
|
146
149
|
generation++;
|
|
147
150
|
itemsCache = null;
|
|
151
|
+
linesCache = null;
|
|
148
152
|
countCache = null;
|
|
149
153
|
isAuthoritative = false;
|
|
150
154
|
pendingRequest = null;
|
|
@@ -269,6 +273,24 @@ function buildGiftItems(items) {
|
|
|
269
273
|
}
|
|
270
274
|
return gifts;
|
|
271
275
|
}
|
|
276
|
+
/** Every line of the cart, in cart order — see `CartProductLine`. */
|
|
277
|
+
function buildProductLines(items) {
|
|
278
|
+
const lines = [];
|
|
279
|
+
for (const item of items) {
|
|
280
|
+
if (item?.id === undefined || item?.id === null)
|
|
281
|
+
continue;
|
|
282
|
+
const quantity = toNumber(item.quantity);
|
|
283
|
+
lines.push({
|
|
284
|
+
id: String(item.id),
|
|
285
|
+
productId: toNullableNumber(item.product_id),
|
|
286
|
+
quantity,
|
|
287
|
+
// Only a line the server flagged as granted by a cart offer has free units.
|
|
288
|
+
freeQuantity: item.added_by_offer ? Math.max(0, resolveFreeQuantity(item, quantity)) : 0,
|
|
289
|
+
offerId: item.added_by_offer ? toNullableNumber(item.offer_id) : null,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
return lines;
|
|
293
|
+
}
|
|
272
294
|
function fetchAndCache() {
|
|
273
295
|
if (pendingRequest)
|
|
274
296
|
return pendingRequest;
|
|
@@ -283,6 +305,7 @@ function fetchAndCache() {
|
|
|
283
305
|
// Both projections come out of the SAME response, in one pass.
|
|
284
306
|
return {
|
|
285
307
|
gifts: buildGiftItems(items),
|
|
308
|
+
lines: buildProductLines(items),
|
|
286
309
|
count: extractCartCount(res, found),
|
|
287
310
|
// The response was reached and parsed — these values are the cart, so the cache
|
|
288
311
|
// they land in may be short-circuited on. See `isAuthoritative`.
|
|
@@ -302,6 +325,7 @@ function fetchAndCache() {
|
|
|
302
325
|
// connection can never claim the cart is empty.
|
|
303
326
|
.catch((error) => ({
|
|
304
327
|
gifts: itemsCache ?? [],
|
|
328
|
+
lines: linesCache ?? [],
|
|
305
329
|
count: isCartNotCreated(error) ? 0 : countCache,
|
|
306
330
|
/**
|
|
307
331
|
* NOT authoritative, `CartNotCreated` included.
|
|
@@ -318,13 +342,14 @@ function fetchAndCache() {
|
|
|
318
342
|
*/
|
|
319
343
|
ok: false,
|
|
320
344
|
}))
|
|
321
|
-
.then(({ gifts, count, ok }) => {
|
|
345
|
+
.then(({ gifts, lines, count, ok }) => {
|
|
322
346
|
// Only publish if nothing invalidated us while we were in flight.
|
|
323
347
|
// Otherwise this is pre-mutation data racing a newer, authoritative
|
|
324
348
|
// request — publishing it would make the lookups short-circuit on stale
|
|
325
349
|
// data until the NEXT mutation happens to come along.
|
|
326
350
|
if (startedAtGeneration === generation) {
|
|
327
351
|
itemsCache = gifts;
|
|
352
|
+
linesCache = lines;
|
|
328
353
|
countCache = count;
|
|
329
354
|
isAuthoritative = ok;
|
|
330
355
|
}
|
|
@@ -353,6 +378,20 @@ function getCartFreeGiftItems() {
|
|
|
353
378
|
}
|
|
354
379
|
return fetchAndCache().then(() => itemsCache ?? EMPTY_GIFT_ITEMS);
|
|
355
380
|
}
|
|
381
|
+
/**
|
|
382
|
+
* Every line of the cart (paid and free), in cart order — for the CP-2171 free product picker.
|
|
383
|
+
*
|
|
384
|
+
* Shares the cache, the in-flight promise and the generation guard with
|
|
385
|
+
* `getCartFreeGiftItems()` and `getCartItemCount()`: no request of its own, and never a
|
|
386
|
+
* different cart state from theirs.
|
|
387
|
+
*/
|
|
388
|
+
function getCartProductLines() {
|
|
389
|
+
syncCartIdentity();
|
|
390
|
+
if (isAuthoritative && itemsCache) {
|
|
391
|
+
return Promise.resolve(linesCache ?? EMPTY_PRODUCT_LINES);
|
|
392
|
+
}
|
|
393
|
+
return fetchAndCache().then(() => linesCache ?? EMPTY_PRODUCT_LINES);
|
|
394
|
+
}
|
|
356
395
|
/**
|
|
357
396
|
* Whether the cached cart picture came from a response that was actually reached and
|
|
358
397
|
* parsed, rather than from the fallback a rejected lookup publishes.
|
|
@@ -425,6 +464,7 @@ function refreshCartFreeGifts() {
|
|
|
425
464
|
return pendingRefresh;
|
|
426
465
|
generation++;
|
|
427
466
|
itemsCache = null;
|
|
467
|
+
linesCache = null;
|
|
428
468
|
countCache = null;
|
|
429
469
|
isAuthoritative = false;
|
|
430
470
|
// This invalidation already covers whatever id the cart has now.
|
|
@@ -663,8 +703,13 @@ function sortByThreshold(items, getThreshold, getIdentity) {
|
|
|
663
703
|
* @param rawTierValues each stage's OWN `current_value`, aligned the same way. A tier
|
|
664
704
|
* without a readable one is measured by `currentValue`, so omitting it — an older
|
|
665
705
|
* backend — changes nothing. See `OfferStageProgress.values`.
|
|
706
|
+
* @param rawCanAddFree each stage's `can_add_free` (CP-2171), aligned the same way. Read
|
|
707
|
+
* only in case (3), where no tier carries `is_applied`: a free product tier that
|
|
708
|
+
* carries one is reached when the ENGINE says so, not when its `current_value` —
|
|
709
|
+
* which counts the gift lines at full price — passes `min_spend`. Omit it and case
|
|
710
|
+
* (3) is the threshold behaviour, unchanged.
|
|
666
711
|
*/
|
|
667
|
-
function computeOfferStageProgress(currentValue, rawThresholds, rawStartValue = 0, rawApplied = [], rawTierValues = []) {
|
|
712
|
+
function computeOfferStageProgress(currentValue, rawThresholds, rawStartValue = 0, rawApplied = [], rawTierValues = [], rawCanAddFree = []) {
|
|
668
713
|
// Unparseable thresholds are dropped, not zeroed — see `isUsableThreshold`.
|
|
669
714
|
//
|
|
670
715
|
// The flag travels WITH its threshold through both the drop and the sort. Sorting two
|
|
@@ -677,6 +722,7 @@ function computeOfferStageProgress(currentValue, rawThresholds, rawStartValue =
|
|
|
677
722
|
threshold: parseNumeric(threshold),
|
|
678
723
|
applied: readAppliedFlag(rawApplied?.[index]),
|
|
679
724
|
tierValue: parseNumeric(rawTierValues?.[index]),
|
|
725
|
+
canAddFree: typeof rawCanAddFree?.[index] === 'boolean' ? rawCanAddFree[index] : undefined,
|
|
680
726
|
}))
|
|
681
727
|
.filter((entry) => entry.threshold !== null)
|
|
682
728
|
.sort((a, b) => a.threshold - b.threshold || a.index - b.index);
|
|
@@ -764,12 +810,25 @@ function computeOfferStageProgress(currentValue, rawThresholds, rawStartValue =
|
|
|
764
810
|
appliedIndex = index;
|
|
765
811
|
}
|
|
766
812
|
}
|
|
767
|
-
// Case (3) closes the prefix at
|
|
813
|
+
// Case (3) closes the prefix at the last reached tier rather than comparing tier by tier.
|
|
768
814
|
// With one value for every tier the two are the same statement; with per-tier values
|
|
769
815
|
// it is what keeps `met` a prefix, on the same single-winner reasoning as case (1).
|
|
816
|
+
//
|
|
817
|
+
// CP-2171 — a tier that carries `can_add_free` is reached on the engine's answer, not on
|
|
818
|
+
// its value: a guest with 40 paid and three listed gifts in the cart has a value past
|
|
819
|
+
// `min_spend`, yet every gift is charged. Only `met` reads it; the bar (`lastReachedIndex`,
|
|
820
|
+
// `fills`, `isComplete`) stays the threshold projection, and the guidance line, finding
|
|
821
|
+
// nothing left to spend, says the conditions are not met.
|
|
822
|
+
let lastMetIndex = NO_STAGE_REACHED;
|
|
823
|
+
for (let index = 0; index < thresholds.length; index++) {
|
|
824
|
+
const canAddFree = entries[index].canAddFree;
|
|
825
|
+
if (canAddFree === undefined ? values[index] >= thresholds[index] : canAddFree) {
|
|
826
|
+
lastMetIndex = index;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
770
829
|
const met = hasEngineVerdict
|
|
771
830
|
? thresholds.map((_, index) => index <= appliedIndex)
|
|
772
|
-
: thresholds.map((_, index) => index <=
|
|
831
|
+
: thresholds.map((_, index) => index <= lastMetIndex);
|
|
773
832
|
const metCount = met.filter(Boolean).length;
|
|
774
833
|
const nextUnmetIndex = met.findIndex(isMet => !isMet); // -1 === NO_STAGE_REACHED
|
|
775
834
|
const allMet = nextUnmetIndex === NO_STAGE_REACHED;
|
|
@@ -917,7 +976,7 @@ function getRemainingToStage(index, progress, measure = OfferStageMeasure.CURREN
|
|
|
917
976
|
return rounded > 0 ? rounded : 1 / factor;
|
|
918
977
|
}
|
|
919
978
|
|
|
920
|
-
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}";
|
|
979
|
+
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}";
|
|
921
980
|
|
|
922
981
|
/** Root of every translation key this component uses. */
|
|
923
982
|
const LANG = "blocks.cart_offer";
|
|
@@ -982,6 +1041,19 @@ const RELOCATION_ANCESTOR_LIMIT = 3;
|
|
|
982
1041
|
* DOM with no cart in it. Past this deadline the full-width banner is the final answer.
|
|
983
1042
|
*/
|
|
984
1043
|
const RELOCATION_WATCH_MS = 5000;
|
|
1044
|
+
/**
|
|
1045
|
+
* CP-2171 — how long the picker stays locked after an add succeeded when no fresh cart
|
|
1046
|
+
* lines arrive (a missed `cart::updated`). Long enough to cover the coalescing window and the
|
|
1047
|
+
* cart lookup; short enough that a lost event never leaves Add ignoring taps for long. Past it
|
|
1048
|
+
* the lock goes and the lines are read again — see `unlockOnStaleLines`.
|
|
1049
|
+
*/
|
|
1050
|
+
const FREE_PRODUCT_UNLOCK_FALLBACK_MS = 5000;
|
|
1051
|
+
/** CP-2171 — how long the cart page fetch for new lines may take before falling back to a reload. */
|
|
1052
|
+
const CART_PAGE_FETCH_TIMEOUT_MS = 8000;
|
|
1053
|
+
/** `sessionStorage` key of the cart page reload guard (see `reloadForMissingCartLines`). */
|
|
1054
|
+
const CART_PAGE_RELOAD_KEY = 'salla-conditional-offer:reloaded-for';
|
|
1055
|
+
/** A theme's cart line forms. Product option forms (`.cart-options`) carry an `item-` id too, but are not lines. */
|
|
1056
|
+
const CART_LINE_FORMS = 'form[id^="item-"]:not(.cart-options form)';
|
|
985
1057
|
/**
|
|
986
1058
|
* Clear space kept between the ⓘ popover and the viewport edge when it is nudged back
|
|
987
1059
|
* inside. Purely cosmetic — one gutter, both edges, both directions.
|
|
@@ -1014,10 +1086,32 @@ const EN_FALLBACKS = {
|
|
|
1014
1086
|
details_title: 'Offer details',
|
|
1015
1087
|
free_gift: 'Free Gift',
|
|
1016
1088
|
free_products_title: 'Free products selected from the offer',
|
|
1089
|
+
// CP-5033 — the gift tier is reached but none of its gifts can be added.
|
|
1090
|
+
free_products_unavailable: 'The free products are currently unavailable',
|
|
1091
|
+
// CP-2171 — the free product picker.
|
|
1092
|
+
free_products_choose: 'Choose :count free products',
|
|
1093
|
+
// Its own key, not :count = 2: Arabic uses the dual form, with no number.
|
|
1094
|
+
free_products_choose_two: 'Choose 2 free products',
|
|
1095
|
+
free_product_choose_one: 'Choose your free product',
|
|
1096
|
+
// The banner line at the gift tier: "…Choose it/them now" while no gift is picked yet, then
|
|
1097
|
+
// the plain line once one is. Same one / two / :count split as the picker title.
|
|
1098
|
+
free_product_got_one: 'You got a free product',
|
|
1099
|
+
free_product_got_pick_one: 'You got a free product! Choose it now',
|
|
1100
|
+
free_products_got: 'You got :count free products',
|
|
1101
|
+
free_products_got_pick: 'You got :count free products! Choose them now',
|
|
1102
|
+
free_products_got_pick_two: 'You got 2 free products! Choose them now',
|
|
1103
|
+
free_products_got_two: 'You got 2 free products',
|
|
1104
|
+
// A picker tier's reward in "Add :remaining and get :reward" and the tooltip rows, instead of
|
|
1105
|
+
// the server's count-less "Free Gift". Same one / two / :count split.
|
|
1106
|
+
free_product_reward_one: 'a free product',
|
|
1107
|
+
free_products_reward: ':count free products',
|
|
1108
|
+
free_products_reward_two: '2 free products',
|
|
1017
1109
|
guidance_conditions_unmet: 'Your cart doesn’t meet the conditions for the next reward yet',
|
|
1018
1110
|
guidance_next: 'Add :remaining and get :reward',
|
|
1019
1111
|
guidance_next_generic: 'Add :remaining to unlock the next reward',
|
|
1020
1112
|
manage_offer: 'Manage offer',
|
|
1113
|
+
// The picker's toggle. Its own key, so offers without a picker keep master's "Manage offer".
|
|
1114
|
+
offer_toggle: 'Offer',
|
|
1021
1115
|
tier_requirement: 'Spend :threshold and get :reward',
|
|
1022
1116
|
tier_requirement_count: 'Buy :threshold and get :reward',
|
|
1023
1117
|
tier_requirement_count_generic: 'Buy :threshold to unlock this offer',
|
|
@@ -1027,6 +1121,15 @@ const EN_FALLBACKS = {
|
|
|
1027
1121
|
/** The gift price label reuses the platform's existing "free of charge" string. */
|
|
1028
1122
|
const FREE_LABEL_KEY = 'pages.checkout.free_charge';
|
|
1029
1123
|
const FREE_LABEL_FALLBACK = 'Free';
|
|
1124
|
+
/**
|
|
1125
|
+
* CP-2171 — the picker's add label is the platform's shared "Add", not a cart-offer key of its
|
|
1126
|
+
* own: the dictionaries keep one string per meaning.
|
|
1127
|
+
*/
|
|
1128
|
+
const ADD_LABEL_KEY = 'common.elements.add';
|
|
1129
|
+
const ADD_LABEL_FALLBACK = 'Add';
|
|
1130
|
+
// The "Free products" panel title reuses the loyalty program's existing "Free products".
|
|
1131
|
+
const FREE_PRODUCTS_TITLE_KEY = 'pages.loyalty_program.free_product_title';
|
|
1132
|
+
const FREE_PRODUCTS_TITLE_FALLBACK = 'Free products';
|
|
1030
1133
|
/**
|
|
1031
1134
|
* Escape a merchant-controlled string before it is substituted into a translation
|
|
1032
1135
|
* template that we then render with `innerHTML`.
|
|
@@ -1133,6 +1236,37 @@ const SallaConditionalOffer = class {
|
|
|
1133
1236
|
this.canRender = true;
|
|
1134
1237
|
/** Free-gift cart LINES belonging to this offer — the "ادارة العرض" card. */
|
|
1135
1238
|
this.gifts = [];
|
|
1239
|
+
/** CP-2171 — every cart line (paid and free), for the picker's per-product state. */
|
|
1240
|
+
this.cartLines = [];
|
|
1241
|
+
/**
|
|
1242
|
+
* CP-2171 — whether `cartLines` came from a cart lookup that answered. Until then the
|
|
1243
|
+
* lines are `[]`: the "Free products" panel and the gift-pick banner line wait for it.
|
|
1244
|
+
*/
|
|
1245
|
+
this.cartLinesLoaded = false;
|
|
1246
|
+
/** CP-2171 — the picker's product cards, keyed by ENCODED product id. */
|
|
1247
|
+
this.freeProductCards = {};
|
|
1248
|
+
/**
|
|
1249
|
+
* CP-2171 — the product (encoded id) an add is in flight for. Held until cart lines read
|
|
1250
|
+
* AFTER the add arrive, not just until the request ends: the cart refresh starts only after
|
|
1251
|
+
* the coalescing window, and a tap in between is ignored, so a double tap adds one unit.
|
|
1252
|
+
*/
|
|
1253
|
+
this.pendingFreeProduct = null;
|
|
1254
|
+
/** `giftsGeneration` when the pending change succeeded; a cart load started after it unlocks. */
|
|
1255
|
+
this.pendingFreeProductSettledAt = null;
|
|
1256
|
+
this.pendingFreeProductTimer = null;
|
|
1257
|
+
/**
|
|
1258
|
+
* `giftsGeneration` when an add from the picker succeeded: the first cart lines read after it
|
|
1259
|
+
* are checked against the page (see `showMissingCartLines`). Kept apart from the
|
|
1260
|
+
* button lock on purpose: the lock's fallback can fire before those lines arrive, and the
|
|
1261
|
+
* page still has to show the new line when they do.
|
|
1262
|
+
*/
|
|
1263
|
+
this.cartPageCheckAfter = null;
|
|
1264
|
+
/** A cart page fetch for missing lines is in flight; lines read meanwhile wait in `pendingCartPageLines`. */
|
|
1265
|
+
this.insertingCartLines = false;
|
|
1266
|
+
this.pendingCartPageLines = null;
|
|
1267
|
+
/** Re-reads of the cart after a failed lookup while a picker change waits; see `retryPickerCartLookup`. */
|
|
1268
|
+
this.pickerLookupRetries = 0;
|
|
1269
|
+
this.pickerLookupTimer = null;
|
|
1136
1270
|
/**
|
|
1137
1271
|
* How many LINES the cart holds — `null` while that is genuinely UNKNOWN.
|
|
1138
1272
|
*
|
|
@@ -1398,6 +1532,9 @@ const SallaConditionalOffer = class {
|
|
|
1398
1532
|
clearTimeout(this.cartRefreshTimer);
|
|
1399
1533
|
this.cartRefreshTimer = null;
|
|
1400
1534
|
}
|
|
1535
|
+
// A dead host has no picker to unlock; drop the fallback timer with the rest.
|
|
1536
|
+
this.unlockFreeProduct();
|
|
1537
|
+
this.clearPickerLookupRetry();
|
|
1401
1538
|
// Likewise: the timer goes, `cartLookupRetryPending` stays.
|
|
1402
1539
|
if (this.cartLookupRetryTimer !== null) {
|
|
1403
1540
|
clearTimeout(this.cartLookupRetryTimer);
|
|
@@ -1887,6 +2024,8 @@ const SallaConditionalOffer = class {
|
|
|
1887
2024
|
return;
|
|
1888
2025
|
this.cartRefreshPending = false;
|
|
1889
2026
|
this.refresh();
|
|
2027
|
+
// A new cart change starts a new picker retry episode: this pass asks again itself.
|
|
2028
|
+
this.clearPickerLookupRetry();
|
|
1890
2029
|
refreshCartFreeGifts().then(() => this.loadCart(), () => this.loadCart());
|
|
1891
2030
|
}
|
|
1892
2031
|
/**
|
|
@@ -1953,6 +2092,14 @@ const SallaConditionalOffer = class {
|
|
|
1953
2092
|
const text = salla.lang.get(FREE_LABEL_KEY);
|
|
1954
2093
|
return text && text !== FREE_LABEL_KEY ? text : FREE_LABEL_FALLBACK;
|
|
1955
2094
|
}
|
|
2095
|
+
getAddLabel() {
|
|
2096
|
+
return this.sharedLabel(ADD_LABEL_KEY, ADD_LABEL_FALLBACK);
|
|
2097
|
+
}
|
|
2098
|
+
/** A platform-wide label (not `blocks.cart_offer.*`): its own text, or `fallback` when missing. */
|
|
2099
|
+
sharedLabel(key, fallback) {
|
|
2100
|
+
const text = salla.lang.get(key);
|
|
2101
|
+
return text && text !== key ? text : fallback;
|
|
2102
|
+
}
|
|
1956
2103
|
/**
|
|
1957
2104
|
* Read a `blocks.cart_offer.*` translation.
|
|
1958
2105
|
*
|
|
@@ -2082,6 +2229,7 @@ const SallaConditionalOffer = class {
|
|
|
2082
2229
|
// Re-asked now that `this.offer` is known, because the gift filter is keyed to
|
|
2083
2230
|
// the offer id — the early pass in `load()` could only answer the COUNT.
|
|
2084
2231
|
this.loadCart();
|
|
2232
|
+
this.loadFreeProductCards();
|
|
2085
2233
|
}
|
|
2086
2234
|
catch (error) {
|
|
2087
2235
|
if (this.isSuperseded(generation))
|
|
@@ -2146,8 +2294,8 @@ const SallaConditionalOffer = class {
|
|
|
2146
2294
|
*/
|
|
2147
2295
|
loadCart() {
|
|
2148
2296
|
const generation = ++this.giftsGeneration;
|
|
2149
|
-
return Promise.all([getCartFreeGiftItems(), getCartItemCount()])
|
|
2150
|
-
.then(([items, count]) => {
|
|
2297
|
+
return Promise.all([getCartFreeGiftItems(), getCartItemCount(), getCartProductLines()])
|
|
2298
|
+
.then(([items, count, lines]) => {
|
|
2151
2299
|
if (generation !== this.giftsGeneration || this.isDisconnected)
|
|
2152
2300
|
return;
|
|
2153
2301
|
/**
|
|
@@ -2161,6 +2309,8 @@ const SallaConditionalOffer = class {
|
|
|
2161
2309
|
*/
|
|
2162
2310
|
if (this.cartRefreshPending)
|
|
2163
2311
|
return;
|
|
2312
|
+
// Was this pass the cart, or a failed lookup's fallback? See below.
|
|
2313
|
+
const answered = hasAuthoritativeCartData();
|
|
2164
2314
|
/**
|
|
2165
2315
|
* NEVER walks a KNOWN count back to `null`. `count` is `null` whenever the
|
|
2166
2316
|
* shared lookup could not answer — which, on the removal path, is every
|
|
@@ -2197,6 +2347,28 @@ const SallaConditionalOffer = class {
|
|
|
2197
2347
|
this.deferredByEmptyCart = false;
|
|
2198
2348
|
this.refresh();
|
|
2199
2349
|
}
|
|
2350
|
+
/**
|
|
2351
|
+
* A failed lookup hands back the cached lines, or `[]` right after a cart
|
|
2352
|
+
* update — not the cart. Publishing them would show an added gift as Add
|
|
2353
|
+
* again (one more tap is a PAID unit) and end the post-change checks below
|
|
2354
|
+
* as though fresh lines had arrived. Keep what we had, and while a picker
|
|
2355
|
+
* change is waiting on real lines — or a shown picker has never had any, so
|
|
2356
|
+
* Add is still disabled — ask again: the count alone schedules no retry once
|
|
2357
|
+
* the cart event has supplied one. A cart with no items (no banner) asks
|
|
2358
|
+
* nothing more.
|
|
2359
|
+
*
|
|
2360
|
+
* An offer without a picker has none of this at stake and goes on as it always
|
|
2361
|
+
* did: its gift card must still follow a removed gift, and nothing would retry.
|
|
2362
|
+
*/
|
|
2363
|
+
const hasPicker = this.getPickerStage(this.getStages()) !== null;
|
|
2364
|
+
const awaitsPickerChange = this.pendingFreeProductSettledAt !== null || this.cartPageCheckAfter !== null;
|
|
2365
|
+
if (!answered && (hasPicker || awaitsPickerChange)) {
|
|
2366
|
+
if (awaitsPickerChange || (!this.cartLinesLoaded && this.hasCartItems())) {
|
|
2367
|
+
this.retryPickerCartLookup();
|
|
2368
|
+
}
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2371
|
+
this.clearPickerLookupRetry();
|
|
2200
2372
|
/**
|
|
2201
2373
|
* The gift card is keyed to the offer, so it can only be filled once the
|
|
2202
2374
|
* offer is known. The early pass fired from `load()` runs BEFORE
|
|
@@ -2206,6 +2378,16 @@ const SallaConditionalOffer = class {
|
|
|
2206
2378
|
* one.
|
|
2207
2379
|
*/
|
|
2208
2380
|
this.gifts = this.offer ? this.selectOwnGifts(items) : [];
|
|
2381
|
+
this.cartLines = this.offer ? lines : [];
|
|
2382
|
+
this.cartLinesLoaded = !!this.offer;
|
|
2383
|
+
// These lines were read after the pending change landed: the picker may act again.
|
|
2384
|
+
if (this.pendingFreeProductSettledAt !== null && generation > this.pendingFreeProductSettledAt) {
|
|
2385
|
+
this.unlockFreeProduct();
|
|
2386
|
+
}
|
|
2387
|
+
if (this.cartPageCheckAfter !== null && generation > this.cartPageCheckAfter) {
|
|
2388
|
+
this.cartPageCheckAfter = null;
|
|
2389
|
+
this.showMissingCartLines(lines);
|
|
2390
|
+
}
|
|
2209
2391
|
})
|
|
2210
2392
|
.catch(error => {
|
|
2211
2393
|
this.safeLog('cart lookup failed', error);
|
|
@@ -2237,6 +2419,27 @@ const SallaConditionalOffer = class {
|
|
|
2237
2419
|
this.loadCart();
|
|
2238
2420
|
}, CART_LOOKUP_RETRY_DELAYS_MS[this.cartLookupRetries]);
|
|
2239
2421
|
}
|
|
2422
|
+
/**
|
|
2423
|
+
* Ask for the cart again after a lookup that could not answer while a picker change is
|
|
2424
|
+
* waiting on real lines. Its own bounded episode, on the count retry's delays: that one
|
|
2425
|
+
* ends as soon as the cart event has supplied a count, which is exactly when this is needed.
|
|
2426
|
+
*/
|
|
2427
|
+
retryPickerCartLookup() {
|
|
2428
|
+
if (this.pickerLookupTimer !== null || this.pickerLookupRetries >= CART_LOOKUP_RETRY_DELAYS_MS.length)
|
|
2429
|
+
return;
|
|
2430
|
+
this.pickerLookupTimer = setTimeout(() => {
|
|
2431
|
+
this.pickerLookupTimer = null;
|
|
2432
|
+
this.pickerLookupRetries++;
|
|
2433
|
+
this.loadCart();
|
|
2434
|
+
}, CART_LOOKUP_RETRY_DELAYS_MS[this.pickerLookupRetries]);
|
|
2435
|
+
}
|
|
2436
|
+
clearPickerLookupRetry() {
|
|
2437
|
+
this.pickerLookupRetries = 0;
|
|
2438
|
+
if (this.pickerLookupTimer === null)
|
|
2439
|
+
return;
|
|
2440
|
+
clearTimeout(this.pickerLookupTimer);
|
|
2441
|
+
this.pickerLookupTimer = null;
|
|
2442
|
+
}
|
|
2240
2443
|
/**
|
|
2241
2444
|
* End the retry episode: a lookup answered, or the cart changed under it. The count
|
|
2242
2445
|
* goes back to zero so a LATER failure gets its own full set of attempts.
|
|
@@ -2293,7 +2496,7 @@ const SallaConditionalOffer = class {
|
|
|
2293
2496
|
* an older backend — is measured by the offer-level value exactly as before.
|
|
2294
2497
|
*/
|
|
2295
2498
|
getProgress(stages) {
|
|
2296
|
-
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));
|
|
2499
|
+
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));
|
|
2297
2500
|
}
|
|
2298
2501
|
/**
|
|
2299
2502
|
* A tier's render key. `id` is the stable identity across refetches; the array
|
|
@@ -2350,6 +2553,15 @@ const SallaConditionalOffer = class {
|
|
|
2350
2553
|
* why every caller has a reward-less sentence to fall back to.
|
|
2351
2554
|
*/
|
|
2352
2555
|
getRewardText(discount) {
|
|
2556
|
+
// CP-2171 — Figma: "Add 140 and get 2 free products". The server's label has no count.
|
|
2557
|
+
if (this.isPickerTier(discount)) {
|
|
2558
|
+
const limit = Math.max(1, Math.floor(Number(discount.max_free_products) || 1));
|
|
2559
|
+
if (limit === 1)
|
|
2560
|
+
return this.trans('free_product_reward_one');
|
|
2561
|
+
if (limit === 2)
|
|
2562
|
+
return this.trans('free_products_reward_two');
|
|
2563
|
+
return this.trans('free_products_reward', { count: `${limit}` });
|
|
2564
|
+
}
|
|
2353
2565
|
return (discount?.reward_label ?? '').trim();
|
|
2354
2566
|
}
|
|
2355
2567
|
/**
|
|
@@ -2371,7 +2583,13 @@ const SallaConditionalOffer = class {
|
|
|
2371
2583
|
getGuidanceMessage(stages, progress) {
|
|
2372
2584
|
if (progress.nextUnmetIndex === NO_STAGE_REACHED)
|
|
2373
2585
|
return '';
|
|
2374
|
-
|
|
2586
|
+
// CP-2171 — the engine's own gap for a picker tier, when the server sends it: `current_value`
|
|
2587
|
+
// counts the gift units the tier would free, so it can read as past the threshold while the
|
|
2588
|
+
// tier is still short ("conditions unmet" with no amount).
|
|
2589
|
+
const engineAmountNeeded = Number(stages[progress.nextUnmetIndex]?.amount_needed);
|
|
2590
|
+
const remainingValue = this.isPickerTier(stages[progress.nextUnmetIndex]) && Number.isFinite(engineAmountNeeded) && engineAmountNeeded > 0
|
|
2591
|
+
? engineAmountNeeded
|
|
2592
|
+
: getRemainingToStage(progress.nextUnmetIndex, progress, this.stageMeasure());
|
|
2375
2593
|
if (remainingValue <= 0)
|
|
2376
2594
|
return this.trans('guidance_conditions_unmet');
|
|
2377
2595
|
const remaining = this.formatRequirement(remainingValue);
|
|
@@ -2649,26 +2867,403 @@ const SallaConditionalOffer = class {
|
|
|
2649
2867
|
}, innerHTML: this.getTierSentence(discount, index, progress) }))));
|
|
2650
2868
|
}))));
|
|
2651
2869
|
}
|
|
2870
|
+
/**
|
|
2871
|
+
* CP-2171 — the tier the free product picker belongs to: the first `free_product` tier that
|
|
2872
|
+
* carries a non-empty `free_products` list. `null` on an offer without one, which keeps the
|
|
2873
|
+
* read-only gift card exactly as before. An empty list (every listed product unavailable) is
|
|
2874
|
+
* treated as no picker: there is nothing to choose, so a "Choose N" header would be a dead end.
|
|
2875
|
+
*/
|
|
2876
|
+
getPickerStage(stages) {
|
|
2877
|
+
return stages.find(stage => this.isPickerTier(stage)) ?? null;
|
|
2878
|
+
}
|
|
2879
|
+
isPickerTier(stage) {
|
|
2880
|
+
return stage?.type === DiscountType.FREE_PRODUCT && Array.isArray(stage.free_products) && stage.free_products.length > 0;
|
|
2881
|
+
}
|
|
2882
|
+
/**
|
|
2883
|
+
* CP-5033 — the furthest tier the cart's value reaches is a gift tier with none of its gifts
|
|
2884
|
+
* addable (the server lists only what can be added: out of stock, hidden and deleted are left
|
|
2885
|
+
* out), and the cart holds none of this offer's gifts. The engine pays nothing for that tier,
|
|
2886
|
+
* so neither "congrats" nor "conditions unmet" is true.
|
|
2887
|
+
*/
|
|
2888
|
+
areGiftsUnavailable(stages, progress) {
|
|
2889
|
+
const stage = stages[progress.lastReachedIndex];
|
|
2890
|
+
return stage?.type === DiscountType.FREE_PRODUCT
|
|
2891
|
+
&& Array.isArray(stage.free_products) && stage.free_products.length === 0
|
|
2892
|
+
&& this.gifts.length === 0;
|
|
2893
|
+
}
|
|
2894
|
+
/**
|
|
2895
|
+
* Load the picker's product cards (name, image, price) — one `salla.product.fetch` for
|
|
2896
|
+
* the ids not loaded yet. Never rejects: a failed lookup leaves the cards out, not the banner.
|
|
2897
|
+
*/
|
|
2898
|
+
loadFreeProductCards() {
|
|
2899
|
+
const stage = this.getPickerStage(this.getStages());
|
|
2900
|
+
const ids = (stage?.free_products ?? [])
|
|
2901
|
+
.map(ref => String(ref.id))
|
|
2902
|
+
.filter(id => id && !this.freeProductCards[id]);
|
|
2903
|
+
if (!ids.length)
|
|
2904
|
+
return Promise.resolve();
|
|
2905
|
+
const listedIds = new Set((stage.free_products ?? []).map(ref => String(ref.id)));
|
|
2906
|
+
return Promise.resolve()
|
|
2907
|
+
.then(() => salla.product.fetch({ source: 'selected', source_value: ids }))
|
|
2908
|
+
.then((response) => {
|
|
2909
|
+
if (this.isDisconnected)
|
|
2910
|
+
return;
|
|
2911
|
+
const cards = { ...this.freeProductCards };
|
|
2912
|
+
(Array.isArray(response?.data) ? response.data : []).forEach((product) => {
|
|
2913
|
+
const id = String(product?.id ?? '');
|
|
2914
|
+
if (!id || !listedIds.has(id))
|
|
2915
|
+
return;
|
|
2916
|
+
cards[id] = {
|
|
2917
|
+
id,
|
|
2918
|
+
name: String(product.name ?? ''),
|
|
2919
|
+
imageUrl: String(product.image?.url ?? ''),
|
|
2920
|
+
imageAlt: String(product.image?.alt ?? product.name ?? ''),
|
|
2921
|
+
price: Number.isFinite(Number(product.price)) ? Number(product.price) : null,
|
|
2922
|
+
};
|
|
2923
|
+
});
|
|
2924
|
+
this.freeProductCards = cards;
|
|
2925
|
+
})
|
|
2926
|
+
.catch(error => this.safeLog('free product lookup failed', error));
|
|
2927
|
+
}
|
|
2928
|
+
/**
|
|
2929
|
+
* The picker's view of the cart for this offer.
|
|
2930
|
+
*
|
|
2931
|
+
* - `freeCount`: products a line of THIS offer made free — the engine's own verdict, read
|
|
2932
|
+
* off the cart lines (`added_by_offer` + `offer_id`), never re-derived here. Gifts of other
|
|
2933
|
+
* offers (another cart offer, Buy X Get Y) are not counted against this offer's limit.
|
|
2934
|
+
* - `reached`: the tier's threshold is met. Read from `current_value` against `min_spend`,
|
|
2935
|
+
* NOT from `is_applied`: a free product tier is only "applied" once a listed product is
|
|
2936
|
+
* in the cart, so gating "Add" on it would never let the customer add the first one.
|
|
2937
|
+
*/
|
|
2938
|
+
getPickerState(stage) {
|
|
2939
|
+
const allFreeLines = this.cartLines.filter(line => line.freeQuantity > 0);
|
|
2940
|
+
const freeLines = this.selectOwnFreeLines(allFreeLines);
|
|
2941
|
+
const free = new Set(freeLines.map(line => line.productId).filter(id => id !== null));
|
|
2942
|
+
const limit = Math.max(1, Math.floor(Number(stage.max_free_products) || 1));
|
|
2943
|
+
// The engine's answer when the server sends it; `current_value >= min_spend` is only a
|
|
2944
|
+
// fallback — it is gross of earlier discounts, so it can say "choose" while the engine
|
|
2945
|
+
// would still charge for the product.
|
|
2946
|
+
const value = Number(stage.current_value ?? this.offer?.details?.current_value ?? 0);
|
|
2947
|
+
const reached = typeof stage.can_add_free === 'boolean'
|
|
2948
|
+
? stage.can_add_free
|
|
2949
|
+
: Number.isFinite(value) && value >= Number(stage.min_spend);
|
|
2950
|
+
return { limit, reached, freeCount: free.size };
|
|
2951
|
+
}
|
|
2952
|
+
/**
|
|
2953
|
+
* The free lines this offer granted — the SAME rule as `selectOwnGifts`, so the picker and
|
|
2954
|
+
* the gifts card never disagree: lines matching the offer id; only when none match, the
|
|
2955
|
+
* lines whose `offer_id` is unreadable.
|
|
2956
|
+
*/
|
|
2957
|
+
selectOwnFreeLines(lines) {
|
|
2958
|
+
const offerId = Number(this.offer?.id);
|
|
2959
|
+
if (!Number.isFinite(offerId))
|
|
2960
|
+
return lines;
|
|
2961
|
+
const owned = lines.filter(line => line.offerId !== null && Number(line.offerId) === offerId);
|
|
2962
|
+
return owned.length ? owned : lines.filter(line => line.offerId === null);
|
|
2963
|
+
}
|
|
2964
|
+
/**
|
|
2965
|
+
* Add a free product to the cart — always from the cart page, never a redirect.
|
|
2966
|
+
*
|
|
2967
|
+
* Quick-add, exactly like the single free product was auto-added before CP-2171: a variant
|
|
2968
|
+
* (SKU) product gets its default SKU — `is_default` first, or the first in stock when stock
|
|
2969
|
+
* is per SKU (`AddToCart::addItemToCart()`) — with that SKU's option values. Any listed
|
|
2970
|
+
* product in the cart counts, so the engine then frees it. A failed add is left to the SDK,
|
|
2971
|
+
* which already shows the shopper its error notice.
|
|
2972
|
+
*/
|
|
2973
|
+
addFreeProduct(card) {
|
|
2974
|
+
if (this.pendingFreeProduct)
|
|
2975
|
+
return;
|
|
2976
|
+
this.pendingFreeProduct = card.id;
|
|
2977
|
+
this.runFreeProductChange(() => Promise.resolve(salla.cart.addItem(card.id)).then(() => { this.cartPageCheckAfter = this.giftsGeneration; }), 'free product add failed');
|
|
2978
|
+
}
|
|
2979
|
+
/** Its own method so specs can observe it; the mock window cannot reload. */
|
|
2980
|
+
reloadPage() {
|
|
2981
|
+
window.location.reload();
|
|
2982
|
+
}
|
|
2983
|
+
/**
|
|
2984
|
+
* Its own method so specs can stub it; the mock window has no `fetch`. Aborted after
|
|
2985
|
+
* `CART_PAGE_FETCH_TIMEOUT_MS`: a fetch that never settles would hold `insertingCartLines`
|
|
2986
|
+
* for the page view, and every later line would only be queued behind it.
|
|
2987
|
+
*/
|
|
2988
|
+
fetchCartPageHtml() {
|
|
2989
|
+
const controller = new AbortController();
|
|
2990
|
+
const timer = setTimeout(() => controller.abort(), CART_PAGE_FETCH_TIMEOUT_MS);
|
|
2991
|
+
// `no-store`: a cached copy of the page would lack the new line and force the reload.
|
|
2992
|
+
return fetch(window.location.href, { cache: 'no-store', credentials: 'same-origin', headers: { Accept: 'text/html' }, signal: controller.signal })
|
|
2993
|
+
.then(response => (response.ok ? response.text() : Promise.reject(new Error(`cart page ${response.status}`))))
|
|
2994
|
+
.finally(() => clearTimeout(timer));
|
|
2995
|
+
}
|
|
2996
|
+
/**
|
|
2997
|
+
* Show the cart lines an add from the picker created that the page has no `form#item-<id>`
|
|
2998
|
+
* for. Themes render cart lines on the server and only refresh lines already on the page, so
|
|
2999
|
+
* the new line (and its price) would otherwise appear only after a manual refresh.
|
|
3000
|
+
*
|
|
3001
|
+
* The cart page is fetched again and the missing line forms are inserted, in the server's
|
|
3002
|
+
* order — no reload. The fetched markup is fresh, so its prices need no update. Its inline
|
|
3003
|
+
* handlers and web components work as on a loaded page; only what the theme's own script wires
|
|
3004
|
+
* to a line at page load (e.g. raed's loading overlay on a quantity change) is missing.
|
|
3005
|
+
* Any failure (fetch, a line the fetched page lacks) falls back to `reloadForMissingCartLines`.
|
|
3006
|
+
*
|
|
3007
|
+
* Left alone on a page with no cart line forms at all (not a cart page this can reason about).
|
|
3008
|
+
* One fetch at a time; lines read meanwhile are checked again after it.
|
|
3009
|
+
*/
|
|
3010
|
+
showMissingCartLines(lines) {
|
|
3011
|
+
if (this.insertingCartLines) {
|
|
3012
|
+
this.pendingCartPageLines = lines;
|
|
3013
|
+
return;
|
|
3014
|
+
}
|
|
3015
|
+
const missing = this.missingCartLineIds(lines);
|
|
3016
|
+
if (!missing.length)
|
|
3017
|
+
return;
|
|
3018
|
+
this.insertingCartLines = true;
|
|
3019
|
+
// Started inside the chain, so a synchronous throw (no `fetch`) still reaches `catch` and `finally`.
|
|
3020
|
+
Promise.resolve()
|
|
3021
|
+
.then(() => this.fetchCartPageHtml())
|
|
3022
|
+
.then(html => {
|
|
3023
|
+
if (this.isDisconnected || this.cartChangedDuringPageFetch())
|
|
3024
|
+
return;
|
|
3025
|
+
const stillMissing = this.missingCartLineIds(this.cartLines);
|
|
3026
|
+
if (stillMissing.length && !this.insertCartLineForms(html, stillMissing)) {
|
|
3027
|
+
this.reloadForMissingCartLines(stillMissing);
|
|
3028
|
+
}
|
|
3029
|
+
})
|
|
3030
|
+
.catch(error => {
|
|
3031
|
+
this.safeLog('cart page fetch for the new lines failed, reloading', error);
|
|
3032
|
+
if (this.isDisconnected || this.cartChangedDuringPageFetch())
|
|
3033
|
+
return;
|
|
3034
|
+
this.reloadForMissingCartLines(this.missingCartLineIds(this.cartLines));
|
|
3035
|
+
})
|
|
3036
|
+
.finally(() => {
|
|
3037
|
+
this.insertingCartLines = false;
|
|
3038
|
+
const pending = this.pendingCartPageLines;
|
|
3039
|
+
this.pendingCartPageLines = null;
|
|
3040
|
+
if (pending && !this.isDisconnected)
|
|
3041
|
+
this.showMissingCartLines(pending);
|
|
3042
|
+
});
|
|
3043
|
+
}
|
|
3044
|
+
/**
|
|
3045
|
+
* A picker change started while the cart page was fetched (e.g. a second add),
|
|
3046
|
+
* so neither the fetched page nor the cart lines can be trusted: inserting now could bring
|
|
3047
|
+
* back a line the cart no longer has. The answer is dropped and the cart read after that
|
|
3048
|
+
* change checks the page again (the change itself arms `cartPageCheckAfter` when it lands;
|
|
3049
|
+
* armed here too, for a change that fails and so arms nothing).
|
|
3050
|
+
*
|
|
3051
|
+
* Lines are always read from `cartLines` once the page arrives, never from the call's own
|
|
3052
|
+
* `lines`: a change that landed during the fetch has already replaced them there.
|
|
3053
|
+
*/
|
|
3054
|
+
cartChangedDuringPageFetch() {
|
|
3055
|
+
// Lines not read since a change (the lock timed out on them) are no basis either.
|
|
3056
|
+
if (this.pendingFreeProduct === null && this.cartLinesLoaded)
|
|
3057
|
+
return false;
|
|
3058
|
+
if (this.cartPageCheckAfter === null)
|
|
3059
|
+
this.cartPageCheckAfter = this.giftsGeneration;
|
|
3060
|
+
return true;
|
|
3061
|
+
}
|
|
3062
|
+
/** Ids of `lines` the page has no form for, sorted; empty on a page with no cart line forms. */
|
|
3063
|
+
missingCartLineIds(lines) {
|
|
3064
|
+
try {
|
|
3065
|
+
if (!document.querySelector(CART_LINE_FORMS))
|
|
3066
|
+
return [];
|
|
3067
|
+
return lines.map(line => line.id).filter(id => !document.getElementById(`item-${id}`)).sort();
|
|
3068
|
+
}
|
|
3069
|
+
catch (error) {
|
|
3070
|
+
this.safeLog('cart page check after a free product add failed', error);
|
|
3071
|
+
return [];
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
/**
|
|
3075
|
+
* Insert the `missing` line forms from the fetched cart page `html`, each right before the next
|
|
3076
|
+
* line the page already has (in the fetched order), else after the last one. Returns false —
|
|
3077
|
+
* nothing inserted — when the fetched page lacks any of them.
|
|
3078
|
+
*/
|
|
3079
|
+
insertCartLineForms(html, missing) {
|
|
3080
|
+
try {
|
|
3081
|
+
const fetched = Array.from(new DOMParser().parseFromString(html, 'text/html').querySelectorAll(CART_LINE_FORMS));
|
|
3082
|
+
// `getAttribute`, not `form.id`: a line form holds an `<input name="id">`, which shadows the property.
|
|
3083
|
+
const fetchedIds = fetched.map(form => form.getAttribute('id'));
|
|
3084
|
+
if (missing.some(id => !fetchedIds.includes(`item-${id}`)))
|
|
3085
|
+
return false;
|
|
3086
|
+
fetched.forEach((form, index) => {
|
|
3087
|
+
const id = fetchedIds[index];
|
|
3088
|
+
if (!missing.includes(id.slice('item-'.length)) || document.getElementById(id))
|
|
3089
|
+
return;
|
|
3090
|
+
// Moved, not cloned: inserting adopts it into this document, and its web components
|
|
3091
|
+
// upgrade once connected. Its inline handlers (`onchange`, delete `onclick`) come along.
|
|
3092
|
+
const next = fetchedIds.slice(index + 1).map(nextId => document.getElementById(nextId)).find(Boolean);
|
|
3093
|
+
if (next) {
|
|
3094
|
+
next.parentNode.insertBefore(form, next);
|
|
3095
|
+
}
|
|
3096
|
+
else {
|
|
3097
|
+
const onPage = document.querySelectorAll(CART_LINE_FORMS);
|
|
3098
|
+
const last = onPage[onPage.length - 1];
|
|
3099
|
+
last.parentNode.insertBefore(form, last.nextSibling);
|
|
3100
|
+
}
|
|
3101
|
+
});
|
|
3102
|
+
return missing.every(id => document.getElementById(`item-${id}`));
|
|
3103
|
+
}
|
|
3104
|
+
catch (error) {
|
|
3105
|
+
this.safeLog('cart line insert failed', error);
|
|
3106
|
+
return false;
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
/**
|
|
3110
|
+
* Reload once, when the missing lines could not be inserted. Never twice for the same missing
|
|
3111
|
+
* lines, so a line a theme never renders cannot loop the page. The guard lives in
|
|
3112
|
+
* `sessionStorage`; where storage is blocked the page still reloads, but never twice in a row.
|
|
3113
|
+
*/
|
|
3114
|
+
reloadForMissingCartLines(missing) {
|
|
3115
|
+
if (!missing.length)
|
|
3116
|
+
return;
|
|
3117
|
+
const signature = missing.join(',');
|
|
3118
|
+
const stored = this.readReloadGuard();
|
|
3119
|
+
if (stored === signature)
|
|
3120
|
+
return;
|
|
3121
|
+
if (stored === undefined && this.pageWasReloaded())
|
|
3122
|
+
return;
|
|
3123
|
+
this.writeReloadGuard(signature);
|
|
3124
|
+
this.reloadPage();
|
|
3125
|
+
}
|
|
3126
|
+
/** The reload guard, or `undefined` when storage cannot be read (blocked, disabled). */
|
|
3127
|
+
readReloadGuard() {
|
|
3128
|
+
try {
|
|
3129
|
+
return window.sessionStorage.getItem(CART_PAGE_RELOAD_KEY);
|
|
3130
|
+
}
|
|
3131
|
+
catch {
|
|
3132
|
+
return undefined;
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
writeReloadGuard(signature) {
|
|
3136
|
+
try {
|
|
3137
|
+
window.sessionStorage.setItem(CART_PAGE_RELOAD_KEY, signature);
|
|
3138
|
+
}
|
|
3139
|
+
catch {
|
|
3140
|
+
// Storage full or blocked: pageWasReloaded() is the guard on the next load.
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
/** Whether this page load is itself a reload — the guard when storage is unavailable. */
|
|
3144
|
+
pageWasReloaded() {
|
|
3145
|
+
try {
|
|
3146
|
+
const navigation = window.performance?.getEntriesByType?.('navigation')?.[0];
|
|
3147
|
+
return navigation?.type === 'reload';
|
|
3148
|
+
}
|
|
3149
|
+
catch {
|
|
3150
|
+
return false;
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
/**
|
|
3154
|
+
* Run one add. A failure unlocks at once — nothing changed. A success stays locked
|
|
3155
|
+
* until a cart load that started after it publishes (see `loadCart`), or the fallback fires.
|
|
3156
|
+
*/
|
|
3157
|
+
runFreeProductChange(change, failure) {
|
|
3158
|
+
Promise.resolve()
|
|
3159
|
+
.then(change)
|
|
3160
|
+
.then(() => {
|
|
3161
|
+
if (this.isDisconnected)
|
|
3162
|
+
return this.unlockFreeProduct();
|
|
3163
|
+
this.pendingFreeProductSettledAt = this.giftsGeneration;
|
|
3164
|
+
this.pendingFreeProductTimer = setTimeout(() => this.unlockOnStaleLines(), FREE_PRODUCT_UNLOCK_FALLBACK_MS);
|
|
3165
|
+
})
|
|
3166
|
+
.catch(error => {
|
|
3167
|
+
this.safeLog(failure, error);
|
|
3168
|
+
this.unlockFreeProduct();
|
|
3169
|
+
// A failed change fires no cart event, so nothing else checks a cart page answer
|
|
3170
|
+
// that was dropped while this change was in flight.
|
|
3171
|
+
if (this.cartPageCheckAfter !== null)
|
|
3172
|
+
this.loadCart();
|
|
3173
|
+
});
|
|
3174
|
+
}
|
|
3175
|
+
/**
|
|
3176
|
+
* The lock's fallback: no cart lines read after the change have arrived, so the lines on
|
|
3177
|
+
* screen are the ones from BEFORE it. Release the lock, mark the lines unread and ask again
|
|
3178
|
+
* on a fresh read — `loadCart` retries within its bound while the picker has no lines.
|
|
3179
|
+
*/
|
|
3180
|
+
unlockOnStaleLines() {
|
|
3181
|
+
this.unlockFreeProduct();
|
|
3182
|
+
this.cartLinesLoaded = false;
|
|
3183
|
+
this.clearPickerLookupRetry();
|
|
3184
|
+
refreshCartFreeGifts().then(() => this.loadCart(), () => this.loadCart());
|
|
3185
|
+
}
|
|
3186
|
+
unlockFreeProduct() {
|
|
3187
|
+
if (this.pendingFreeProductTimer !== null)
|
|
3188
|
+
clearTimeout(this.pendingFreeProductTimer);
|
|
3189
|
+
this.pendingFreeProductTimer = null;
|
|
3190
|
+
this.pendingFreeProductSettledAt = null;
|
|
3191
|
+
this.pendingFreeProduct = null;
|
|
3192
|
+
}
|
|
3193
|
+
/**
|
|
3194
|
+
* CP-2171 — Figma's banner line at the gift tier: "You got N free products", with
|
|
3195
|
+
* "Choose them now" while none is picked yet. N is the tier's limit, not the count picked.
|
|
3196
|
+
*/
|
|
3197
|
+
pickerGotMessage(limit, awaitsPick) {
|
|
3198
|
+
if (limit === 1)
|
|
3199
|
+
return this.trans(awaitsPick ? 'free_product_got_pick_one' : 'free_product_got_one');
|
|
3200
|
+
if (limit === 2)
|
|
3201
|
+
return this.trans(awaitsPick ? 'free_products_got_pick_two' : 'free_products_got_two');
|
|
3202
|
+
return this.trans(awaitsPick ? 'free_products_got_pick' : 'free_products_got', { count: `${limit}` });
|
|
3203
|
+
}
|
|
3204
|
+
getPickerCards(stage) {
|
|
3205
|
+
return (stage.free_products ?? [])
|
|
3206
|
+
.map(ref => this.freeProductCards[String(ref.id)])
|
|
3207
|
+
.filter(Boolean);
|
|
3208
|
+
}
|
|
3209
|
+
/** CP-2171 — Figma "Choose N free products": the counter, then one card per listed product. */
|
|
3210
|
+
renderPicker(stage) {
|
|
3211
|
+
const state = this.getPickerState(stage);
|
|
3212
|
+
const cards = this.getPickerCards(stage);
|
|
3213
|
+
// Cards still loading, or the lookup failed: no header and counter over an empty list.
|
|
3214
|
+
if (!cards.length)
|
|
3215
|
+
return null;
|
|
3216
|
+
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
|
|
3217
|
+
? this.trans('free_product_choose_one')
|
|
3218
|
+
: state.limit === 2
|
|
3219
|
+
? this.trans('free_products_choose_two')
|
|
3220
|
+
: this.trans('free_products_choose', { count: `${state.limit}` }))), h("ul", { class: "s-conditional-offer-picker-list" }, cards.map(card => this.renderPickerCard(card)))));
|
|
3221
|
+
}
|
|
3222
|
+
/**
|
|
3223
|
+
* One picker card: the product, its normal price and Add — the same on every card, whether
|
|
3224
|
+
* the product is in the cart, free or not. Add is never disabled: every tap adds one unit at
|
|
3225
|
+
* the normal price (also once the product is in the cart, and after the free limit is used
|
|
3226
|
+
* up) and the engine decides what is free. What IS free shows in the "Free products" panel
|
|
3227
|
+
* below, never on the card. A tap while the previous add is still in flight is ignored
|
|
3228
|
+
* (`addFreeProduct`), so one double tap is one unit.
|
|
3229
|
+
*
|
|
3230
|
+
* No remove: the picker never deletes a cart line. A free line can also hold paid units, and
|
|
3231
|
+
* deleting it would take what the customer bought with the gift — products leave the cart
|
|
3232
|
+
* from their own cart line only.
|
|
3233
|
+
*/
|
|
3234
|
+
renderPickerCard(card) {
|
|
3235
|
+
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()))));
|
|
3236
|
+
}
|
|
2652
3237
|
/**
|
|
2653
3238
|
* "ادارة العرض". Rendered TWICE with complementary responsive visibility rather than
|
|
2654
3239
|
* moved with `order:`: desktop wants it inline with the banner row, mobile wants a
|
|
2655
3240
|
* full-width bar BELOW the gifts card, and those are different places
|
|
2656
3241
|
* in the flow, not a different order within one flex line.
|
|
2657
3242
|
*/
|
|
2658
|
-
renderGiftsToggle(variant) {
|
|
3243
|
+
renderGiftsToggle(variant, hasPicker) {
|
|
2659
3244
|
return (h("button", { type: "button", id: this.elementId(`toggle-${variant}`), class: {
|
|
2660
3245
|
's-conditional-offer-toggle': true,
|
|
2661
3246
|
's-conditional-offer-toggle-inline': variant === 'inline',
|
|
2662
3247
|
's-conditional-offer-toggle-bar': variant === 'bar',
|
|
2663
|
-
}, "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: {
|
|
3248
|
+
}, "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: {
|
|
2664
3249
|
'sicon-keyboard_arrow_down': true,
|
|
2665
3250
|
's-conditional-offer-toggle-icon': true,
|
|
2666
3251
|
's-conditional-offer-toggle-icon-open': this.isGiftsOpen,
|
|
2667
3252
|
}, "aria-hidden": "true" })));
|
|
2668
3253
|
}
|
|
2669
|
-
/**
|
|
2670
|
-
|
|
2671
|
-
|
|
3254
|
+
/**
|
|
3255
|
+
* Spec §5 — the gifts card: one row per gift line. Read-only on an offer without a picker,
|
|
3256
|
+
* exactly as before; with the CP-2171 picker it becomes the Figma "Free products" panel,
|
|
3257
|
+
* shown only once a gift is in the cart. Its rows are read-only, as are the picker cards: a
|
|
3258
|
+
* gift leaves the cart from its own cart line.
|
|
3259
|
+
*/
|
|
3260
|
+
renderGiftsCard(pickerStage = null) {
|
|
3261
|
+
// No picker: the card exactly as before CP-2171 — no wrapper, so no new box styling.
|
|
3262
|
+
if (!pickerStage) {
|
|
3263
|
+
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)))));
|
|
3264
|
+
}
|
|
3265
|
+
const state = this.getPickerState(pickerStage);
|
|
3266
|
+
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));
|
|
2672
3267
|
}
|
|
2673
3268
|
renderGiftRow(gift) {
|
|
2674
3269
|
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 ? (
|
|
@@ -2715,12 +3310,26 @@ const SallaConditionalOffer = class {
|
|
|
2715
3310
|
const progress = this.getProgress(stages);
|
|
2716
3311
|
const guidance = this.getGuidanceMessage(stages, progress);
|
|
2717
3312
|
const detailsTitle = this.trans('details_title');
|
|
2718
|
-
// Spec §2 — the accordion exists only when there is something to manage
|
|
2719
|
-
|
|
2720
|
-
|
|
3313
|
+
// Spec §2 — the accordion exists only when there is something to manage: gift lines,
|
|
3314
|
+
// or (CP-2171) a free product picker — shown only once the customer has reached its
|
|
3315
|
+
// stage, then before anything is picked too.
|
|
3316
|
+
const pickerStage = this.getPickerStage(stages);
|
|
3317
|
+
const pickerState = pickerStage !== null ? this.getPickerState(pickerStage) : null;
|
|
3318
|
+
const isPickerReached = pickerState?.reached ?? false;
|
|
3319
|
+
const hasGifts = this.gifts.length > 0 || (isPickerReached && this.getPickerCards(pickerStage).length > 0);
|
|
3320
|
+
// The gift tier is reached but none of this offer's gifts is in the cart: the engine
|
|
3321
|
+
// grants that tier only once one is, and pays nothing until then (no fallback to a lower
|
|
3322
|
+
// tier). So neither "congrats" nor "conditions unmet" is true — ask for the pick. Only
|
|
3323
|
+
// once the cart lines are read, or a cart that holds the gift would flash this line.
|
|
3324
|
+
const awaitsGiftPick = isPickerReached && this.cartLinesLoaded && pickerState.freeCount === 0;
|
|
3325
|
+
// "You got N free products" replaces congrats only when the gift tier is the offer's last:
|
|
3326
|
+
// below a higher tier, `guidance` (or congrats for that higher tier) is the truer line.
|
|
3327
|
+
const gotGifts = progress.allMet && isPickerReached && pickerState.freeCount > 0
|
|
3328
|
+
&& stages[stages.length - 1] === pickerStage;
|
|
3329
|
+
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 ? (
|
|
2721
3330
|
// innerHTML: the `:remaining` half carries `salla.money()` markup;
|
|
2722
3331
|
// the reward half is escaped in `getGuidanceMessage`.
|
|
2723
|
-
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));
|
|
3332
|
+
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));
|
|
2724
3333
|
}
|
|
2725
3334
|
get hostEl() { return getElement(this); }
|
|
2726
3335
|
};
|