@salla.sa/twilight-components 3.0.24 → 3.0.26
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-Di1G8EnP.js} +1 -1
- package/dist/cjs/{filepond-BMxlEH8M.js → filepond-DU_-t59i.js} +1 -1
- package/dist/cjs/{filepond-plugin-file-poster-DVSz4StJ.js → filepond-plugin-file-poster-CYpSqb6i.js} +1 -1
- package/dist/cjs/{filepond-plugin-file-validate-size-CXJiLSzt.js → filepond-plugin-file-validate-size-a_tUhUBx.js} +1 -1
- package/dist/cjs/{filepond-plugin-file-validate-type-DWK85msl.js → filepond-plugin-file-validate-type-D_X7irXu.js} +1 -1
- package/dist/cjs/{filepond-plugin-image-edit-BHttJI5X.js → filepond-plugin-image-edit-BRP3GZ31.js} +1 -1
- package/dist/cjs/{filepond-plugin-image-exif-orientation-DTKIO2zE.js → filepond-plugin-image-exif-orientation-CiHIpg-4.js} +1 -1
- package/dist/cjs/{filepond-plugin-image-preview-BaRxwhFa.js → filepond-plugin-image-preview-CKT6x8T1.js} +1 -1
- package/dist/cjs/{index-BJ3-fFSF.js → index-BLPUgwXq.js} +6 -6
- package/dist/cjs/info-Do5bVPmK.js +13 -0
- package/dist/cjs/{intlTelInput-DNoOb73M.js → intlTelInput-CyL5UDfU.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 +3 -9
- 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 +120 -5
- 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 +27 -6
- 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-C9AVuYe1.js} +1 -1
- package/dist/collection/components/salla-cart-item-offers/salla-cart-item-offers.js +4 -0
- package/dist/collection/components/salla-cart-summary-card/salla-cart-summary-card.css +152 -59
- package/dist/collection/components/salla-cart-summary-card/salla-cart-summary-card.js +123 -4
- 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/components/salla-order-totals-card/salla-order-totals-card.css +3 -0
- package/dist/collection/components/salla-order-totals-card/salla-order-totals-card.js +25 -4
- 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/info.js +11 -0
- package/dist/components/salla-bottom-alert.js +2 -8
- package/dist/components/salla-cart-item-offers.js +4 -0
- package/dist/components/salla-cart-summary-card.js +124 -5
- 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/components/salla-order-totals-card.js +26 -5
- package/dist/docs.json +4 -4
- package/dist/esm/{Helper-BwzPfwZJ.js → Helper-CFEqaSN-.js} +1 -1
- package/dist/esm/{filepond-JNV-1zqR.js → filepond-BfW_OXid.js} +1 -1
- package/dist/esm/{filepond-plugin-file-poster-nFqNHCag.js → filepond-plugin-file-poster-CsMiL-wZ.js} +1 -1
- package/dist/esm/{filepond-plugin-file-validate-size-P3KmOTH8.js → filepond-plugin-file-validate-size-BuxVUFKn.js} +1 -1
- package/dist/esm/{filepond-plugin-file-validate-type-BV0Ctr7U.js → filepond-plugin-file-validate-type-Dq94isoe.js} +1 -1
- package/dist/esm/{filepond-plugin-image-edit-D62rKlfH.js → filepond-plugin-image-edit-CEVstXlA.js} +1 -1
- package/dist/esm/{filepond-plugin-image-exif-orientation-DerapxIk.js → filepond-plugin-image-exif-orientation-BhViRs-X.js} +1 -1
- package/dist/esm/{filepond-plugin-image-preview-BlRd2Ble.js → filepond-plugin-image-preview-BrscXNqJ.js} +1 -1
- package/dist/esm/{index-CtoXzf1y.js → index-g-_4am8C.js} +6 -6
- package/dist/esm/info-OdOOjQZO.js +11 -0
- package/dist/esm/{intlTelInput-BZz-u5_w.js → intlTelInput-PsSXBr__.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 +3 -9
- 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 +120 -5
- 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 +27 -6
- 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-D3lgt9go.js} +1 -1
- package/dist/twilight/{p-0b583810.entry.js → p-02006b3a.entry.js} +1 -1
- package/dist/twilight/{p-f51f31df.entry.js → p-0207dae4.entry.js} +1 -1
- package/dist/twilight/{p-117e714b.entry.js → p-025e30c7.entry.js} +1 -1
- package/dist/twilight/{p-38b70281.entry.js → p-058e4be3.entry.js} +1 -1
- package/dist/twilight/{p-77deaa00.entry.js → p-0ce6514e.entry.js} +1 -1
- package/dist/twilight/{p-f3455825.entry.js → p-0d94c90e.entry.js} +1 -1
- package/dist/twilight/{p-5dea6c09.entry.js → p-0e423424.entry.js} +1 -1
- package/dist/twilight/{p-71b5546b.entry.js → p-0f7db4a1.entry.js} +1 -1
- package/dist/twilight/{p-374d9f0c.entry.js → p-12038aef.entry.js} +1 -1
- package/dist/twilight/{p-2e1dd9cf.entry.js → p-12dcb9a0.entry.js} +1 -1
- package/dist/twilight/{p-aac9d2fe.entry.js → p-14311053.entry.js} +1 -1
- package/dist/twilight/{p-0ce0cdb0.entry.js → p-151df172.entry.js} +1 -1
- package/dist/twilight/{p-d76b7564.entry.js → p-152ac425.entry.js} +1 -1
- package/dist/twilight/{p-a4fee391.entry.js → p-1b6d84a8.entry.js} +1 -1
- package/dist/twilight/p-1d31b983.entry.js +4 -0
- package/dist/twilight/{p-6495383e.entry.js → p-1dcbbbbf.entry.js} +1 -1
- package/dist/twilight/{p-f310b923.entry.js → p-1df3fe4a.entry.js} +1 -1
- package/dist/twilight/{p-00a1ec4e.entry.js → p-22271248.entry.js} +1 -1
- package/dist/twilight/{p-e8ebb2fe.entry.js → p-2834a4d3.entry.js} +1 -1
- package/dist/twilight/{p-2882d626.entry.js → p-2907057d.entry.js} +1 -1
- package/dist/twilight/{p-ed37e702.entry.js → p-2d3b29d9.entry.js} +1 -1
- package/dist/twilight/{p-055bf228.entry.js → p-2db9ee05.entry.js} +1 -1
- package/dist/twilight/{p-4080fdc9.entry.js → p-2e4ce75b.entry.js} +1 -1
- package/dist/twilight/{p-3845e427.entry.js → p-329b7cc2.entry.js} +1 -1
- package/dist/twilight/{p-c4264e87.entry.js → p-3c2e7bef.entry.js} +1 -1
- package/dist/twilight/p-3fedd652.entry.js +4 -0
- package/dist/twilight/{p-36a7344c.entry.js → p-4247db1a.entry.js} +1 -1
- package/dist/twilight/{p-f664fa69.entry.js → p-496f950b.entry.js} +1 -1
- package/dist/twilight/{p-40e651ac.entry.js → p-49cd89d1.entry.js} +1 -1
- package/dist/twilight/p-4b8bb969.entry.js +4 -0
- package/dist/twilight/{p-c18fe9e5.entry.js → p-5166cd84.entry.js} +1 -1
- package/dist/twilight/{p-cbff6c68.entry.js → p-5350ba07.entry.js} +1 -1
- package/dist/twilight/{p-2c04bba3.entry.js → p-54eec74a.entry.js} +1 -1
- package/dist/twilight/{p-ae1efde9.entry.js → p-55861492.entry.js} +1 -1
- package/dist/twilight/{p-cd55369c.entry.js → p-6070b995.entry.js} +1 -1
- package/dist/twilight/{p-4254e9c2.entry.js → p-62a3394a.entry.js} +1 -1
- package/dist/twilight/{p-974c100d.entry.js → p-63c982e1.entry.js} +1 -1
- package/dist/twilight/{p-2afc4658.entry.js → p-6432401e.entry.js} +1 -1
- package/dist/twilight/{p-7a8fde62.entry.js → p-64ae09b4.entry.js} +1 -1
- package/dist/twilight/{p-f67d03fb.entry.js → p-685a61fa.entry.js} +1 -1
- package/dist/twilight/{p-7c684782.entry.js → p-710a42d2.entry.js} +1 -1
- package/dist/twilight/{p-2566f688.entry.js → p-72e13a12.entry.js} +1 -1
- package/dist/twilight/{p-ebed682b.entry.js → p-73612f04.entry.js} +1 -1
- package/dist/twilight/{p-2401f0d7.entry.js → p-74602eb0.entry.js} +1 -1
- package/dist/twilight/{p-365ff361.entry.js → p-76e238b3.entry.js} +1 -1
- package/dist/twilight/{p-7169ba9a.entry.js → p-78240cb3.entry.js} +1 -1
- package/dist/twilight/{p-8413d52b.entry.js → p-78a7b46e.entry.js} +1 -1
- package/dist/twilight/{p-1af75625.entry.js → p-79f9403a.entry.js} +1 -1
- package/dist/twilight/{p-6ffbf505.entry.js → p-7f4f300a.entry.js} +1 -1
- package/dist/twilight/{p-2140be84.entry.js → p-7fc9a0bf.entry.js} +1 -1
- package/dist/twilight/{p-b1bc70af.entry.js → p-8016f122.entry.js} +1 -1
- package/dist/twilight/{p-a8102290.entry.js → p-801eb7d8.entry.js} +1 -1
- package/dist/twilight/{p-02ddd64c.entry.js → p-81ec67d8.entry.js} +1 -1
- package/dist/twilight/{p-3356638d.entry.js → p-83a1123b.entry.js} +1 -1
- package/dist/twilight/p-84b9416e.entry.js +4 -0
- package/dist/twilight/{p-8fe950e8.entry.js → p-8783aa81.entry.js} +1 -1
- package/dist/twilight/{p-60ae422e.entry.js → p-87ba0ca2.entry.js} +1 -1
- package/dist/twilight/{p-e2cb5c49.entry.js → p-8cbbe961.entry.js} +1 -1
- package/dist/twilight/{p-a260f35e.entry.js → p-8ec7866e.entry.js} +1 -1
- package/dist/twilight/{p-e89daada.entry.js → p-92cbac49.entry.js} +1 -1
- package/dist/twilight/{p-3da2e3f3.entry.js → p-93a85463.entry.js} +1 -1
- package/dist/twilight/{p-4f4933f8.entry.js → p-95f8caa0.entry.js} +1 -1
- package/dist/twilight/{p-061216d4.entry.js → p-965c3ee8.entry.js} +1 -1
- package/dist/twilight/{p-9c1b0978.entry.js → p-98b24f1d.entry.js} +1 -1
- package/dist/twilight/{p-6KZIgnh6.js → p-9h9-vkr9.js} +1 -1
- package/dist/twilight/{p-DT73iyFr.js → p-B6WlUws-.js} +1 -1
- package/dist/twilight/{p-78r0jdyY.js → p-ByR59DTl.js} +2 -2
- package/dist/twilight/{p-DYAn-F48.js → p-CB552NRi.js} +2 -2
- package/dist/twilight/{p-x4_Sxqd_.js → p-CWi90iJ1.js} +1 -1
- package/dist/twilight/{p-CC-KFXD9.js → p-DjCyIWR1.js} +1 -1
- package/dist/twilight/{p-BeC8AhMn.js → p-EXd4paYi.js} +1 -1
- package/dist/twilight/{p-BwUX8dew.js → p-MEqjtc_i.js} +1 -1
- package/dist/twilight/p-OdOOjQZO.js +4 -0
- package/dist/twilight/{p-D5DrmlyQ.js → p-WsiD2WP-.js} +1 -1
- package/dist/twilight/{p-F1Erq7GR.js → p-_ulhlUUw.js} +1 -1
- package/dist/twilight/{p-b3472096.entry.js → p-a185d836.entry.js} +1 -1
- package/dist/twilight/p-a2147ef3.entry.js +4 -0
- package/dist/twilight/{p-1d105a57.entry.js → p-a21bc7ee.entry.js} +1 -1
- package/dist/twilight/{p-1f39ca4f.entry.js → p-a255167f.entry.js} +1 -1
- package/dist/twilight/{p-72b0724c.entry.js → p-a76a2a5d.entry.js} +1 -1
- package/dist/twilight/{p-97065b4f.entry.js → p-a7aa162c.entry.js} +1 -1
- package/dist/twilight/{p-c1ab1b40.entry.js → p-aa5be7cf.entry.js} +1 -1
- package/dist/twilight/{p-0d1b8490.entry.js → p-ac31a382.entry.js} +1 -1
- package/dist/twilight/{p-94d5b080.entry.js → p-ad95bb9a.entry.js} +1 -1
- package/dist/twilight/{p-0cb60522.entry.js → p-ae4ea8c9.entry.js} +1 -1
- package/dist/twilight/{p-d6a3b2d8.entry.js → p-ba601791.entry.js} +1 -1
- package/dist/twilight/{p-dc2be7b8.entry.js → p-c0423cfc.entry.js} +1 -1
- package/dist/twilight/{p-a236dd3b.entry.js → p-c861b955.entry.js} +1 -1
- package/dist/twilight/{p-3d3eda0f.entry.js → p-c96e9150.entry.js} +1 -1
- package/dist/twilight/{p-1b4c3b85.entry.js → p-cdd8e44a.entry.js} +1 -1
- package/dist/twilight/{p-9e50f29c.entry.js → p-cf3285f1.entry.js} +1 -1
- package/dist/twilight/p-d02dee6d.entry.js +4 -0
- package/dist/twilight/{p-95e7c2e1.entry.js → p-d204326c.entry.js} +1 -1
- package/dist/twilight/{p-e5dc1d27.entry.js → p-d304b8fc.entry.js} +1 -1
- package/dist/twilight/{p-79b743eb.entry.js → p-da63cbf0.entry.js} +1 -1
- package/dist/twilight/p-db2b9499.entry.js +4 -0
- package/dist/twilight/{p-a9ae6ab9.entry.js → p-dba5b89a.entry.js} +1 -1
- package/dist/twilight/{p-aca2717e.entry.js → p-e0387b79.entry.js} +1 -1
- package/dist/twilight/{p-f738f492.entry.js → p-e0dc2c2d.entry.js} +1 -1
- package/dist/twilight/{p-3e285056.entry.js → p-e3d37ab2.entry.js} +1 -1
- package/dist/twilight/{p-b3c9ab0e.entry.js → p-e59a8901.entry.js} +1 -1
- package/dist/twilight/{p-4adaee32.entry.js → p-e776afcd.entry.js} +1 -1
- package/dist/twilight/{p-4008e164.entry.js → p-e8921db3.entry.js} +1 -1
- package/dist/twilight/p-ec9434d9.entry.js +4 -0
- package/dist/twilight/{p-4c5a7ffc.entry.js → p-ee7e6ce9.entry.js} +1 -1
- package/dist/twilight/{p-213368ef.entry.js → p-f0ab3933.entry.js} +1 -1
- package/dist/twilight/{p-8ea3a265.entry.js → p-f16bdaf5.entry.js} +1 -1
- package/dist/twilight/{p-e8198efc.entry.js → p-f2718aec.entry.js} +1 -1
- package/dist/twilight/{p-118aa92e.entry.js → p-f581bb32.entry.js} +1 -1
- package/dist/twilight/{p-8fd6ed52.entry.js → p-f6981856.entry.js} +1 -1
- package/dist/twilight/{p-b2cbdaa0.entry.js → p-f7f8d758.entry.js} +1 -1
- package/dist/twilight/{p-fbce8a18.entry.js → p-fbff3efd.entry.js} +1 -1
- package/dist/twilight/{p-36167cbe.entry.js → p-ff890108.entry.js} +1 -1
- package/dist/twilight/{p-8cbf3bc2.entry.js → p-fffd562d.entry.js} +1 -1
- package/dist/twilight/{p-CtoXzf1y.js → p-g-_4am8C.js} +1 -1
- package/dist/twilight/twilight.esm.js +1 -1
- package/dist/types/components/salla-cart-summary-card/salla-cart-summary-card.d.ts +26 -0
- 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/components/salla-order-totals-card/interfaces.d.ts +8 -3
- package/dist/types/components/salla-order-totals-card/salla-order-totals-card.d.ts +2 -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-034b80ad.entry.js +0 -4
- 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-7303ebd1.entry.js +0 -4
- package/dist/twilight/p-8b9c44c8.entry.js +0 -4
- package/dist/twilight/p-919650b7.entry.js +0 -4
- package/dist/twilight/p-ebe45961.entry.js +0 -4
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
'use strict';
|
|
5
5
|
|
|
6
|
-
var index = require('./index-
|
|
6
|
+
var index = require('./index-BLPUgwXq.js');
|
|
7
7
|
|
|
8
8
|
var DiscountType;
|
|
9
9
|
(function (DiscountType) {
|
|
@@ -46,6 +46,9 @@ var OfferType;
|
|
|
46
46
|
* `twilight/packages/twilight/src/api/cart.js:831-846`.
|
|
47
47
|
*/
|
|
48
48
|
const EMPTY_GIFT_ITEMS = Object.freeze([]);
|
|
49
|
+
const EMPTY_PRODUCT_LINES = Object.freeze([]);
|
|
50
|
+
/** Every line from the last parsed response — published in the SAME pass as `itemsCache`. */
|
|
51
|
+
let linesCache = null;
|
|
49
52
|
/**
|
|
50
53
|
* The gift LINES from the last parsed response. `null` is the single "nothing has loaded
|
|
51
54
|
* yet" flag shared with `countCache`, which keeps the two from ever disagreeing about
|
|
@@ -147,6 +150,7 @@ function syncCartIdentity() {
|
|
|
147
150
|
if (cacheIdentity !== undefined && cacheIdentity !== current) {
|
|
148
151
|
generation++;
|
|
149
152
|
itemsCache = null;
|
|
153
|
+
linesCache = null;
|
|
150
154
|
countCache = null;
|
|
151
155
|
isAuthoritative = false;
|
|
152
156
|
pendingRequest = null;
|
|
@@ -271,6 +275,24 @@ function buildGiftItems(items) {
|
|
|
271
275
|
}
|
|
272
276
|
return gifts;
|
|
273
277
|
}
|
|
278
|
+
/** Every line of the cart, in cart order — see `CartProductLine`. */
|
|
279
|
+
function buildProductLines(items) {
|
|
280
|
+
const lines = [];
|
|
281
|
+
for (const item of items) {
|
|
282
|
+
if (item?.id === undefined || item?.id === null)
|
|
283
|
+
continue;
|
|
284
|
+
const quantity = toNumber(item.quantity);
|
|
285
|
+
lines.push({
|
|
286
|
+
id: String(item.id),
|
|
287
|
+
productId: toNullableNumber(item.product_id),
|
|
288
|
+
quantity,
|
|
289
|
+
// Only a line the server flagged as granted by a cart offer has free units.
|
|
290
|
+
freeQuantity: item.added_by_offer ? Math.max(0, resolveFreeQuantity(item, quantity)) : 0,
|
|
291
|
+
offerId: item.added_by_offer ? toNullableNumber(item.offer_id) : null,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
return lines;
|
|
295
|
+
}
|
|
274
296
|
function fetchAndCache() {
|
|
275
297
|
if (pendingRequest)
|
|
276
298
|
return pendingRequest;
|
|
@@ -285,6 +307,7 @@ function fetchAndCache() {
|
|
|
285
307
|
// Both projections come out of the SAME response, in one pass.
|
|
286
308
|
return {
|
|
287
309
|
gifts: buildGiftItems(items),
|
|
310
|
+
lines: buildProductLines(items),
|
|
288
311
|
count: extractCartCount(res, found),
|
|
289
312
|
// The response was reached and parsed — these values are the cart, so the cache
|
|
290
313
|
// they land in may be short-circuited on. See `isAuthoritative`.
|
|
@@ -304,6 +327,7 @@ function fetchAndCache() {
|
|
|
304
327
|
// connection can never claim the cart is empty.
|
|
305
328
|
.catch((error) => ({
|
|
306
329
|
gifts: itemsCache ?? [],
|
|
330
|
+
lines: linesCache ?? [],
|
|
307
331
|
count: isCartNotCreated(error) ? 0 : countCache,
|
|
308
332
|
/**
|
|
309
333
|
* NOT authoritative, `CartNotCreated` included.
|
|
@@ -320,13 +344,14 @@ function fetchAndCache() {
|
|
|
320
344
|
*/
|
|
321
345
|
ok: false,
|
|
322
346
|
}))
|
|
323
|
-
.then(({ gifts, count, ok }) => {
|
|
347
|
+
.then(({ gifts, lines, count, ok }) => {
|
|
324
348
|
// Only publish if nothing invalidated us while we were in flight.
|
|
325
349
|
// Otherwise this is pre-mutation data racing a newer, authoritative
|
|
326
350
|
// request — publishing it would make the lookups short-circuit on stale
|
|
327
351
|
// data until the NEXT mutation happens to come along.
|
|
328
352
|
if (startedAtGeneration === generation) {
|
|
329
353
|
itemsCache = gifts;
|
|
354
|
+
linesCache = lines;
|
|
330
355
|
countCache = count;
|
|
331
356
|
isAuthoritative = ok;
|
|
332
357
|
}
|
|
@@ -355,6 +380,20 @@ function getCartFreeGiftItems() {
|
|
|
355
380
|
}
|
|
356
381
|
return fetchAndCache().then(() => itemsCache ?? EMPTY_GIFT_ITEMS);
|
|
357
382
|
}
|
|
383
|
+
/**
|
|
384
|
+
* Every line of the cart (paid and free), in cart order — for the CP-2171 free product picker.
|
|
385
|
+
*
|
|
386
|
+
* Shares the cache, the in-flight promise and the generation guard with
|
|
387
|
+
* `getCartFreeGiftItems()` and `getCartItemCount()`: no request of its own, and never a
|
|
388
|
+
* different cart state from theirs.
|
|
389
|
+
*/
|
|
390
|
+
function getCartProductLines() {
|
|
391
|
+
syncCartIdentity();
|
|
392
|
+
if (isAuthoritative && itemsCache) {
|
|
393
|
+
return Promise.resolve(linesCache ?? EMPTY_PRODUCT_LINES);
|
|
394
|
+
}
|
|
395
|
+
return fetchAndCache().then(() => linesCache ?? EMPTY_PRODUCT_LINES);
|
|
396
|
+
}
|
|
358
397
|
/**
|
|
359
398
|
* Whether the cached cart picture came from a response that was actually reached and
|
|
360
399
|
* parsed, rather than from the fallback a rejected lookup publishes.
|
|
@@ -427,6 +466,7 @@ function refreshCartFreeGifts() {
|
|
|
427
466
|
return pendingRefresh;
|
|
428
467
|
generation++;
|
|
429
468
|
itemsCache = null;
|
|
469
|
+
linesCache = null;
|
|
430
470
|
countCache = null;
|
|
431
471
|
isAuthoritative = false;
|
|
432
472
|
// This invalidation already covers whatever id the cart has now.
|
|
@@ -665,8 +705,13 @@ function sortByThreshold(items, getThreshold, getIdentity) {
|
|
|
665
705
|
* @param rawTierValues each stage's OWN `current_value`, aligned the same way. A tier
|
|
666
706
|
* without a readable one is measured by `currentValue`, so omitting it — an older
|
|
667
707
|
* backend — changes nothing. See `OfferStageProgress.values`.
|
|
708
|
+
* @param rawCanAddFree each stage's `can_add_free` (CP-2171), aligned the same way. Read
|
|
709
|
+
* only in case (3), where no tier carries `is_applied`: a free product tier that
|
|
710
|
+
* carries one is reached when the ENGINE says so, not when its `current_value` —
|
|
711
|
+
* which counts the gift lines at full price — passes `min_spend`. Omit it and case
|
|
712
|
+
* (3) is the threshold behaviour, unchanged.
|
|
668
713
|
*/
|
|
669
|
-
function computeOfferStageProgress(currentValue, rawThresholds, rawStartValue = 0, rawApplied = [], rawTierValues = []) {
|
|
714
|
+
function computeOfferStageProgress(currentValue, rawThresholds, rawStartValue = 0, rawApplied = [], rawTierValues = [], rawCanAddFree = []) {
|
|
670
715
|
// Unparseable thresholds are dropped, not zeroed — see `isUsableThreshold`.
|
|
671
716
|
//
|
|
672
717
|
// The flag travels WITH its threshold through both the drop and the sort. Sorting two
|
|
@@ -679,6 +724,7 @@ function computeOfferStageProgress(currentValue, rawThresholds, rawStartValue =
|
|
|
679
724
|
threshold: parseNumeric(threshold),
|
|
680
725
|
applied: readAppliedFlag(rawApplied?.[index]),
|
|
681
726
|
tierValue: parseNumeric(rawTierValues?.[index]),
|
|
727
|
+
canAddFree: typeof rawCanAddFree?.[index] === 'boolean' ? rawCanAddFree[index] : undefined,
|
|
682
728
|
}))
|
|
683
729
|
.filter((entry) => entry.threshold !== null)
|
|
684
730
|
.sort((a, b) => a.threshold - b.threshold || a.index - b.index);
|
|
@@ -766,12 +812,25 @@ function computeOfferStageProgress(currentValue, rawThresholds, rawStartValue =
|
|
|
766
812
|
appliedIndex = index;
|
|
767
813
|
}
|
|
768
814
|
}
|
|
769
|
-
// Case (3) closes the prefix at
|
|
815
|
+
// Case (3) closes the prefix at the last reached tier rather than comparing tier by tier.
|
|
770
816
|
// With one value for every tier the two are the same statement; with per-tier values
|
|
771
817
|
// it is what keeps `met` a prefix, on the same single-winner reasoning as case (1).
|
|
818
|
+
//
|
|
819
|
+
// CP-2171 — a tier that carries `can_add_free` is reached on the engine's answer, not on
|
|
820
|
+
// its value: a guest with 40 paid and three listed gifts in the cart has a value past
|
|
821
|
+
// `min_spend`, yet every gift is charged. Only `met` reads it; the bar (`lastReachedIndex`,
|
|
822
|
+
// `fills`, `isComplete`) stays the threshold projection, and the guidance line, finding
|
|
823
|
+
// nothing left to spend, says the conditions are not met.
|
|
824
|
+
let lastMetIndex = NO_STAGE_REACHED;
|
|
825
|
+
for (let index = 0; index < thresholds.length; index++) {
|
|
826
|
+
const canAddFree = entries[index].canAddFree;
|
|
827
|
+
if (canAddFree === undefined ? values[index] >= thresholds[index] : canAddFree) {
|
|
828
|
+
lastMetIndex = index;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
772
831
|
const met = hasEngineVerdict
|
|
773
832
|
? thresholds.map((_, index) => index <= appliedIndex)
|
|
774
|
-
: thresholds.map((_, index) => index <=
|
|
833
|
+
: thresholds.map((_, index) => index <= lastMetIndex);
|
|
775
834
|
const metCount = met.filter(Boolean).length;
|
|
776
835
|
const nextUnmetIndex = met.findIndex(isMet => !isMet); // -1 === NO_STAGE_REACHED
|
|
777
836
|
const allMet = nextUnmetIndex === NO_STAGE_REACHED;
|
|
@@ -919,7 +978,7 @@ function getRemainingToStage(index, progress, measure = OfferStageMeasure.CURREN
|
|
|
919
978
|
return rounded > 0 ? rounded : 1 / factor;
|
|
920
979
|
}
|
|
921
980
|
|
|
922
|
-
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}";
|
|
981
|
+
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}";
|
|
923
982
|
|
|
924
983
|
/** Root of every translation key this component uses. */
|
|
925
984
|
const LANG = "blocks.cart_offer";
|
|
@@ -984,6 +1043,19 @@ const RELOCATION_ANCESTOR_LIMIT = 3;
|
|
|
984
1043
|
* DOM with no cart in it. Past this deadline the full-width banner is the final answer.
|
|
985
1044
|
*/
|
|
986
1045
|
const RELOCATION_WATCH_MS = 5000;
|
|
1046
|
+
/**
|
|
1047
|
+
* CP-2171 — how long the picker stays locked after an add succeeded when no fresh cart
|
|
1048
|
+
* lines arrive (a missed `cart::updated`). Long enough to cover the coalescing window and the
|
|
1049
|
+
* cart lookup; short enough that a lost event never leaves Add ignoring taps for long. Past it
|
|
1050
|
+
* the lock goes and the lines are read again — see `unlockOnStaleLines`.
|
|
1051
|
+
*/
|
|
1052
|
+
const FREE_PRODUCT_UNLOCK_FALLBACK_MS = 5000;
|
|
1053
|
+
/** CP-2171 — how long the cart page fetch for new lines may take before falling back to a reload. */
|
|
1054
|
+
const CART_PAGE_FETCH_TIMEOUT_MS = 8000;
|
|
1055
|
+
/** `sessionStorage` key of the cart page reload guard (see `reloadForMissingCartLines`). */
|
|
1056
|
+
const CART_PAGE_RELOAD_KEY = 'salla-conditional-offer:reloaded-for';
|
|
1057
|
+
/** A theme's cart line forms. Product option forms (`.cart-options`) carry an `item-` id too, but are not lines. */
|
|
1058
|
+
const CART_LINE_FORMS = 'form[id^="item-"]:not(.cart-options form)';
|
|
987
1059
|
/**
|
|
988
1060
|
* Clear space kept between the ⓘ popover and the viewport edge when it is nudged back
|
|
989
1061
|
* inside. Purely cosmetic — one gutter, both edges, both directions.
|
|
@@ -1016,10 +1088,32 @@ const EN_FALLBACKS = {
|
|
|
1016
1088
|
details_title: 'Offer details',
|
|
1017
1089
|
free_gift: 'Free Gift',
|
|
1018
1090
|
free_products_title: 'Free products selected from the offer',
|
|
1091
|
+
// CP-5033 — the gift tier is reached but none of its gifts can be added.
|
|
1092
|
+
free_products_unavailable: 'The free products are currently unavailable',
|
|
1093
|
+
// CP-2171 — the free product picker.
|
|
1094
|
+
free_products_choose: 'Choose :count free products',
|
|
1095
|
+
// Its own key, not :count = 2: Arabic uses the dual form, with no number.
|
|
1096
|
+
free_products_choose_two: 'Choose 2 free products',
|
|
1097
|
+
free_product_choose_one: 'Choose your free product',
|
|
1098
|
+
// The banner line at the gift tier: "…Choose it/them now" while no gift is picked yet, then
|
|
1099
|
+
// the plain line once one is. Same one / two / :count split as the picker title.
|
|
1100
|
+
free_product_got_one: 'You got a free product',
|
|
1101
|
+
free_product_got_pick_one: 'You got a free product! Choose it now',
|
|
1102
|
+
free_products_got: 'You got :count free products',
|
|
1103
|
+
free_products_got_pick: 'You got :count free products! Choose them now',
|
|
1104
|
+
free_products_got_pick_two: 'You got 2 free products! Choose them now',
|
|
1105
|
+
free_products_got_two: 'You got 2 free products',
|
|
1106
|
+
// A picker tier's reward in "Add :remaining and get :reward" and the tooltip rows, instead of
|
|
1107
|
+
// the server's count-less "Free Gift". Same one / two / :count split.
|
|
1108
|
+
free_product_reward_one: 'a free product',
|
|
1109
|
+
free_products_reward: ':count free products',
|
|
1110
|
+
free_products_reward_two: '2 free products',
|
|
1019
1111
|
guidance_conditions_unmet: 'Your cart doesn’t meet the conditions for the next reward yet',
|
|
1020
1112
|
guidance_next: 'Add :remaining and get :reward',
|
|
1021
1113
|
guidance_next_generic: 'Add :remaining to unlock the next reward',
|
|
1022
1114
|
manage_offer: 'Manage offer',
|
|
1115
|
+
// The picker's toggle. Its own key, so offers without a picker keep master's "Manage offer".
|
|
1116
|
+
offer_toggle: 'Offer',
|
|
1023
1117
|
tier_requirement: 'Spend :threshold and get :reward',
|
|
1024
1118
|
tier_requirement_count: 'Buy :threshold and get :reward',
|
|
1025
1119
|
tier_requirement_count_generic: 'Buy :threshold to unlock this offer',
|
|
@@ -1029,6 +1123,15 @@ const EN_FALLBACKS = {
|
|
|
1029
1123
|
/** The gift price label reuses the platform's existing "free of charge" string. */
|
|
1030
1124
|
const FREE_LABEL_KEY = 'pages.checkout.free_charge';
|
|
1031
1125
|
const FREE_LABEL_FALLBACK = 'Free';
|
|
1126
|
+
/**
|
|
1127
|
+
* CP-2171 — the picker's add label is the platform's shared "Add", not a cart-offer key of its
|
|
1128
|
+
* own: the dictionaries keep one string per meaning.
|
|
1129
|
+
*/
|
|
1130
|
+
const ADD_LABEL_KEY = 'common.elements.add';
|
|
1131
|
+
const ADD_LABEL_FALLBACK = 'Add';
|
|
1132
|
+
// The "Free products" panel title reuses the loyalty program's existing "Free products".
|
|
1133
|
+
const FREE_PRODUCTS_TITLE_KEY = 'pages.loyalty_program.free_product_title';
|
|
1134
|
+
const FREE_PRODUCTS_TITLE_FALLBACK = 'Free products';
|
|
1032
1135
|
/**
|
|
1033
1136
|
* Escape a merchant-controlled string before it is substituted into a translation
|
|
1034
1137
|
* template that we then render with `innerHTML`.
|
|
@@ -1135,6 +1238,37 @@ const SallaConditionalOffer = class {
|
|
|
1135
1238
|
this.canRender = true;
|
|
1136
1239
|
/** Free-gift cart LINES belonging to this offer — the "ادارة العرض" card. */
|
|
1137
1240
|
this.gifts = [];
|
|
1241
|
+
/** CP-2171 — every cart line (paid and free), for the picker's per-product state. */
|
|
1242
|
+
this.cartLines = [];
|
|
1243
|
+
/**
|
|
1244
|
+
* CP-2171 — whether `cartLines` came from a cart lookup that answered. Until then the
|
|
1245
|
+
* lines are `[]`: the "Free products" panel and the gift-pick banner line wait for it.
|
|
1246
|
+
*/
|
|
1247
|
+
this.cartLinesLoaded = false;
|
|
1248
|
+
/** CP-2171 — the picker's product cards, keyed by ENCODED product id. */
|
|
1249
|
+
this.freeProductCards = {};
|
|
1250
|
+
/**
|
|
1251
|
+
* CP-2171 — the product (encoded id) an add is in flight for. Held until cart lines read
|
|
1252
|
+
* AFTER the add arrive, not just until the request ends: the cart refresh starts only after
|
|
1253
|
+
* the coalescing window, and a tap in between is ignored, so a double tap adds one unit.
|
|
1254
|
+
*/
|
|
1255
|
+
this.pendingFreeProduct = null;
|
|
1256
|
+
/** `giftsGeneration` when the pending change succeeded; a cart load started after it unlocks. */
|
|
1257
|
+
this.pendingFreeProductSettledAt = null;
|
|
1258
|
+
this.pendingFreeProductTimer = null;
|
|
1259
|
+
/**
|
|
1260
|
+
* `giftsGeneration` when an add from the picker succeeded: the first cart lines read after it
|
|
1261
|
+
* are checked against the page (see `showMissingCartLines`). Kept apart from the
|
|
1262
|
+
* button lock on purpose: the lock's fallback can fire before those lines arrive, and the
|
|
1263
|
+
* page still has to show the new line when they do.
|
|
1264
|
+
*/
|
|
1265
|
+
this.cartPageCheckAfter = null;
|
|
1266
|
+
/** A cart page fetch for missing lines is in flight; lines read meanwhile wait in `pendingCartPageLines`. */
|
|
1267
|
+
this.insertingCartLines = false;
|
|
1268
|
+
this.pendingCartPageLines = null;
|
|
1269
|
+
/** Re-reads of the cart after a failed lookup while a picker change waits; see `retryPickerCartLookup`. */
|
|
1270
|
+
this.pickerLookupRetries = 0;
|
|
1271
|
+
this.pickerLookupTimer = null;
|
|
1138
1272
|
/**
|
|
1139
1273
|
* How many LINES the cart holds — `null` while that is genuinely UNKNOWN.
|
|
1140
1274
|
*
|
|
@@ -1400,6 +1534,9 @@ const SallaConditionalOffer = class {
|
|
|
1400
1534
|
clearTimeout(this.cartRefreshTimer);
|
|
1401
1535
|
this.cartRefreshTimer = null;
|
|
1402
1536
|
}
|
|
1537
|
+
// A dead host has no picker to unlock; drop the fallback timer with the rest.
|
|
1538
|
+
this.unlockFreeProduct();
|
|
1539
|
+
this.clearPickerLookupRetry();
|
|
1403
1540
|
// Likewise: the timer goes, `cartLookupRetryPending` stays.
|
|
1404
1541
|
if (this.cartLookupRetryTimer !== null) {
|
|
1405
1542
|
clearTimeout(this.cartLookupRetryTimer);
|
|
@@ -1889,6 +2026,8 @@ const SallaConditionalOffer = class {
|
|
|
1889
2026
|
return;
|
|
1890
2027
|
this.cartRefreshPending = false;
|
|
1891
2028
|
this.refresh();
|
|
2029
|
+
// A new cart change starts a new picker retry episode: this pass asks again itself.
|
|
2030
|
+
this.clearPickerLookupRetry();
|
|
1892
2031
|
refreshCartFreeGifts().then(() => this.loadCart(), () => this.loadCart());
|
|
1893
2032
|
}
|
|
1894
2033
|
/**
|
|
@@ -1955,6 +2094,14 @@ const SallaConditionalOffer = class {
|
|
|
1955
2094
|
const text = salla.lang.get(FREE_LABEL_KEY);
|
|
1956
2095
|
return text && text !== FREE_LABEL_KEY ? text : FREE_LABEL_FALLBACK;
|
|
1957
2096
|
}
|
|
2097
|
+
getAddLabel() {
|
|
2098
|
+
return this.sharedLabel(ADD_LABEL_KEY, ADD_LABEL_FALLBACK);
|
|
2099
|
+
}
|
|
2100
|
+
/** A platform-wide label (not `blocks.cart_offer.*`): its own text, or `fallback` when missing. */
|
|
2101
|
+
sharedLabel(key, fallback) {
|
|
2102
|
+
const text = salla.lang.get(key);
|
|
2103
|
+
return text && text !== key ? text : fallback;
|
|
2104
|
+
}
|
|
1958
2105
|
/**
|
|
1959
2106
|
* Read a `blocks.cart_offer.*` translation.
|
|
1960
2107
|
*
|
|
@@ -2084,6 +2231,7 @@ const SallaConditionalOffer = class {
|
|
|
2084
2231
|
// Re-asked now that `this.offer` is known, because the gift filter is keyed to
|
|
2085
2232
|
// the offer id — the early pass in `load()` could only answer the COUNT.
|
|
2086
2233
|
this.loadCart();
|
|
2234
|
+
this.loadFreeProductCards();
|
|
2087
2235
|
}
|
|
2088
2236
|
catch (error) {
|
|
2089
2237
|
if (this.isSuperseded(generation))
|
|
@@ -2148,8 +2296,8 @@ const SallaConditionalOffer = class {
|
|
|
2148
2296
|
*/
|
|
2149
2297
|
loadCart() {
|
|
2150
2298
|
const generation = ++this.giftsGeneration;
|
|
2151
|
-
return Promise.all([getCartFreeGiftItems(), getCartItemCount()])
|
|
2152
|
-
.then(([items, count]) => {
|
|
2299
|
+
return Promise.all([getCartFreeGiftItems(), getCartItemCount(), getCartProductLines()])
|
|
2300
|
+
.then(([items, count, lines]) => {
|
|
2153
2301
|
if (generation !== this.giftsGeneration || this.isDisconnected)
|
|
2154
2302
|
return;
|
|
2155
2303
|
/**
|
|
@@ -2163,6 +2311,8 @@ const SallaConditionalOffer = class {
|
|
|
2163
2311
|
*/
|
|
2164
2312
|
if (this.cartRefreshPending)
|
|
2165
2313
|
return;
|
|
2314
|
+
// Was this pass the cart, or a failed lookup's fallback? See below.
|
|
2315
|
+
const answered = hasAuthoritativeCartData();
|
|
2166
2316
|
/**
|
|
2167
2317
|
* NEVER walks a KNOWN count back to `null`. `count` is `null` whenever the
|
|
2168
2318
|
* shared lookup could not answer — which, on the removal path, is every
|
|
@@ -2199,6 +2349,28 @@ const SallaConditionalOffer = class {
|
|
|
2199
2349
|
this.deferredByEmptyCart = false;
|
|
2200
2350
|
this.refresh();
|
|
2201
2351
|
}
|
|
2352
|
+
/**
|
|
2353
|
+
* A failed lookup hands back the cached lines, or `[]` right after a cart
|
|
2354
|
+
* update — not the cart. Publishing them would show an added gift as Add
|
|
2355
|
+
* again (one more tap is a PAID unit) and end the post-change checks below
|
|
2356
|
+
* as though fresh lines had arrived. Keep what we had, and while a picker
|
|
2357
|
+
* change is waiting on real lines — or a shown picker has never had any, so
|
|
2358
|
+
* Add is still disabled — ask again: the count alone schedules no retry once
|
|
2359
|
+
* the cart event has supplied one. A cart with no items (no banner) asks
|
|
2360
|
+
* nothing more.
|
|
2361
|
+
*
|
|
2362
|
+
* An offer without a picker has none of this at stake and goes on as it always
|
|
2363
|
+
* did: its gift card must still follow a removed gift, and nothing would retry.
|
|
2364
|
+
*/
|
|
2365
|
+
const hasPicker = this.getPickerStage(this.getStages()) !== null;
|
|
2366
|
+
const awaitsPickerChange = this.pendingFreeProductSettledAt !== null || this.cartPageCheckAfter !== null;
|
|
2367
|
+
if (!answered && (hasPicker || awaitsPickerChange)) {
|
|
2368
|
+
if (awaitsPickerChange || (!this.cartLinesLoaded && this.hasCartItems())) {
|
|
2369
|
+
this.retryPickerCartLookup();
|
|
2370
|
+
}
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
this.clearPickerLookupRetry();
|
|
2202
2374
|
/**
|
|
2203
2375
|
* The gift card is keyed to the offer, so it can only be filled once the
|
|
2204
2376
|
* offer is known. The early pass fired from `load()` runs BEFORE
|
|
@@ -2208,6 +2380,16 @@ const SallaConditionalOffer = class {
|
|
|
2208
2380
|
* one.
|
|
2209
2381
|
*/
|
|
2210
2382
|
this.gifts = this.offer ? this.selectOwnGifts(items) : [];
|
|
2383
|
+
this.cartLines = this.offer ? lines : [];
|
|
2384
|
+
this.cartLinesLoaded = !!this.offer;
|
|
2385
|
+
// These lines were read after the pending change landed: the picker may act again.
|
|
2386
|
+
if (this.pendingFreeProductSettledAt !== null && generation > this.pendingFreeProductSettledAt) {
|
|
2387
|
+
this.unlockFreeProduct();
|
|
2388
|
+
}
|
|
2389
|
+
if (this.cartPageCheckAfter !== null && generation > this.cartPageCheckAfter) {
|
|
2390
|
+
this.cartPageCheckAfter = null;
|
|
2391
|
+
this.showMissingCartLines(lines);
|
|
2392
|
+
}
|
|
2211
2393
|
})
|
|
2212
2394
|
.catch(error => {
|
|
2213
2395
|
this.safeLog('cart lookup failed', error);
|
|
@@ -2239,6 +2421,27 @@ const SallaConditionalOffer = class {
|
|
|
2239
2421
|
this.loadCart();
|
|
2240
2422
|
}, CART_LOOKUP_RETRY_DELAYS_MS[this.cartLookupRetries]);
|
|
2241
2423
|
}
|
|
2424
|
+
/**
|
|
2425
|
+
* Ask for the cart again after a lookup that could not answer while a picker change is
|
|
2426
|
+
* waiting on real lines. Its own bounded episode, on the count retry's delays: that one
|
|
2427
|
+
* ends as soon as the cart event has supplied a count, which is exactly when this is needed.
|
|
2428
|
+
*/
|
|
2429
|
+
retryPickerCartLookup() {
|
|
2430
|
+
if (this.pickerLookupTimer !== null || this.pickerLookupRetries >= CART_LOOKUP_RETRY_DELAYS_MS.length)
|
|
2431
|
+
return;
|
|
2432
|
+
this.pickerLookupTimer = setTimeout(() => {
|
|
2433
|
+
this.pickerLookupTimer = null;
|
|
2434
|
+
this.pickerLookupRetries++;
|
|
2435
|
+
this.loadCart();
|
|
2436
|
+
}, CART_LOOKUP_RETRY_DELAYS_MS[this.pickerLookupRetries]);
|
|
2437
|
+
}
|
|
2438
|
+
clearPickerLookupRetry() {
|
|
2439
|
+
this.pickerLookupRetries = 0;
|
|
2440
|
+
if (this.pickerLookupTimer === null)
|
|
2441
|
+
return;
|
|
2442
|
+
clearTimeout(this.pickerLookupTimer);
|
|
2443
|
+
this.pickerLookupTimer = null;
|
|
2444
|
+
}
|
|
2242
2445
|
/**
|
|
2243
2446
|
* End the retry episode: a lookup answered, or the cart changed under it. The count
|
|
2244
2447
|
* goes back to zero so a LATER failure gets its own full set of attempts.
|
|
@@ -2295,7 +2498,7 @@ const SallaConditionalOffer = class {
|
|
|
2295
2498
|
* an older backend — is measured by the offer-level value exactly as before.
|
|
2296
2499
|
*/
|
|
2297
2500
|
getProgress(stages) {
|
|
2298
|
-
return computeOfferStageProgress(this.offer?.details?.current_value, stages.map(({ min_spend }) => min_spend), this.offer?.details?.start_value, stages.map(({ is_applied }) => is_applied), stages.map(({ current_value }) => current_value));
|
|
2501
|
+
return computeOfferStageProgress(this.offer?.details?.current_value, stages.map(({ min_spend }) => min_spend), this.offer?.details?.start_value, stages.map(({ is_applied }) => is_applied), stages.map(({ current_value }) => current_value), stages.map(({ can_add_free }) => can_add_free));
|
|
2299
2502
|
}
|
|
2300
2503
|
/**
|
|
2301
2504
|
* A tier's render key. `id` is the stable identity across refetches; the array
|
|
@@ -2352,6 +2555,15 @@ const SallaConditionalOffer = class {
|
|
|
2352
2555
|
* why every caller has a reward-less sentence to fall back to.
|
|
2353
2556
|
*/
|
|
2354
2557
|
getRewardText(discount) {
|
|
2558
|
+
// CP-2171 — Figma: "Add 140 and get 2 free products". The server's label has no count.
|
|
2559
|
+
if (this.isPickerTier(discount)) {
|
|
2560
|
+
const limit = Math.max(1, Math.floor(Number(discount.max_free_products) || 1));
|
|
2561
|
+
if (limit === 1)
|
|
2562
|
+
return this.trans('free_product_reward_one');
|
|
2563
|
+
if (limit === 2)
|
|
2564
|
+
return this.trans('free_products_reward_two');
|
|
2565
|
+
return this.trans('free_products_reward', { count: `${limit}` });
|
|
2566
|
+
}
|
|
2355
2567
|
return (discount?.reward_label ?? '').trim();
|
|
2356
2568
|
}
|
|
2357
2569
|
/**
|
|
@@ -2373,7 +2585,13 @@ const SallaConditionalOffer = class {
|
|
|
2373
2585
|
getGuidanceMessage(stages, progress) {
|
|
2374
2586
|
if (progress.nextUnmetIndex === NO_STAGE_REACHED)
|
|
2375
2587
|
return '';
|
|
2376
|
-
|
|
2588
|
+
// CP-2171 — the engine's own gap for a picker tier, when the server sends it: `current_value`
|
|
2589
|
+
// counts the gift units the tier would free, so it can read as past the threshold while the
|
|
2590
|
+
// tier is still short ("conditions unmet" with no amount).
|
|
2591
|
+
const engineAmountNeeded = Number(stages[progress.nextUnmetIndex]?.amount_needed);
|
|
2592
|
+
const remainingValue = this.isPickerTier(stages[progress.nextUnmetIndex]) && Number.isFinite(engineAmountNeeded) && engineAmountNeeded > 0
|
|
2593
|
+
? engineAmountNeeded
|
|
2594
|
+
: getRemainingToStage(progress.nextUnmetIndex, progress, this.stageMeasure());
|
|
2377
2595
|
if (remainingValue <= 0)
|
|
2378
2596
|
return this.trans('guidance_conditions_unmet');
|
|
2379
2597
|
const remaining = this.formatRequirement(remainingValue);
|
|
@@ -2651,26 +2869,403 @@ const SallaConditionalOffer = class {
|
|
|
2651
2869
|
}, innerHTML: this.getTierSentence(discount, index$1, progress) }))));
|
|
2652
2870
|
}))));
|
|
2653
2871
|
}
|
|
2872
|
+
/**
|
|
2873
|
+
* CP-2171 — the tier the free product picker belongs to: the first `free_product` tier that
|
|
2874
|
+
* carries a non-empty `free_products` list. `null` on an offer without one, which keeps the
|
|
2875
|
+
* read-only gift card exactly as before. An empty list (every listed product unavailable) is
|
|
2876
|
+
* treated as no picker: there is nothing to choose, so a "Choose N" header would be a dead end.
|
|
2877
|
+
*/
|
|
2878
|
+
getPickerStage(stages) {
|
|
2879
|
+
return stages.find(stage => this.isPickerTier(stage)) ?? null;
|
|
2880
|
+
}
|
|
2881
|
+
isPickerTier(stage) {
|
|
2882
|
+
return stage?.type === DiscountType.FREE_PRODUCT && Array.isArray(stage.free_products) && stage.free_products.length > 0;
|
|
2883
|
+
}
|
|
2884
|
+
/**
|
|
2885
|
+
* CP-5033 — the furthest tier the cart's value reaches is a gift tier with none of its gifts
|
|
2886
|
+
* addable (the server lists only what can be added: out of stock, hidden and deleted are left
|
|
2887
|
+
* out), and the cart holds none of this offer's gifts. The engine pays nothing for that tier,
|
|
2888
|
+
* so neither "congrats" nor "conditions unmet" is true.
|
|
2889
|
+
*/
|
|
2890
|
+
areGiftsUnavailable(stages, progress) {
|
|
2891
|
+
const stage = stages[progress.lastReachedIndex];
|
|
2892
|
+
return stage?.type === DiscountType.FREE_PRODUCT
|
|
2893
|
+
&& Array.isArray(stage.free_products) && stage.free_products.length === 0
|
|
2894
|
+
&& this.gifts.length === 0;
|
|
2895
|
+
}
|
|
2896
|
+
/**
|
|
2897
|
+
* Load the picker's product cards (name, image, price) — one `salla.product.fetch` for
|
|
2898
|
+
* the ids not loaded yet. Never rejects: a failed lookup leaves the cards out, not the banner.
|
|
2899
|
+
*/
|
|
2900
|
+
loadFreeProductCards() {
|
|
2901
|
+
const stage = this.getPickerStage(this.getStages());
|
|
2902
|
+
const ids = (stage?.free_products ?? [])
|
|
2903
|
+
.map(ref => String(ref.id))
|
|
2904
|
+
.filter(id => id && !this.freeProductCards[id]);
|
|
2905
|
+
if (!ids.length)
|
|
2906
|
+
return Promise.resolve();
|
|
2907
|
+
const listedIds = new Set((stage.free_products ?? []).map(ref => String(ref.id)));
|
|
2908
|
+
return Promise.resolve()
|
|
2909
|
+
.then(() => salla.product.fetch({ source: 'selected', source_value: ids }))
|
|
2910
|
+
.then((response) => {
|
|
2911
|
+
if (this.isDisconnected)
|
|
2912
|
+
return;
|
|
2913
|
+
const cards = { ...this.freeProductCards };
|
|
2914
|
+
(Array.isArray(response?.data) ? response.data : []).forEach((product) => {
|
|
2915
|
+
const id = String(product?.id ?? '');
|
|
2916
|
+
if (!id || !listedIds.has(id))
|
|
2917
|
+
return;
|
|
2918
|
+
cards[id] = {
|
|
2919
|
+
id,
|
|
2920
|
+
name: String(product.name ?? ''),
|
|
2921
|
+
imageUrl: String(product.image?.url ?? ''),
|
|
2922
|
+
imageAlt: String(product.image?.alt ?? product.name ?? ''),
|
|
2923
|
+
price: Number.isFinite(Number(product.price)) ? Number(product.price) : null,
|
|
2924
|
+
};
|
|
2925
|
+
});
|
|
2926
|
+
this.freeProductCards = cards;
|
|
2927
|
+
})
|
|
2928
|
+
.catch(error => this.safeLog('free product lookup failed', error));
|
|
2929
|
+
}
|
|
2930
|
+
/**
|
|
2931
|
+
* The picker's view of the cart for this offer.
|
|
2932
|
+
*
|
|
2933
|
+
* - `freeCount`: products a line of THIS offer made free — the engine's own verdict, read
|
|
2934
|
+
* off the cart lines (`added_by_offer` + `offer_id`), never re-derived here. Gifts of other
|
|
2935
|
+
* offers (another cart offer, Buy X Get Y) are not counted against this offer's limit.
|
|
2936
|
+
* - `reached`: the tier's threshold is met. Read from `current_value` against `min_spend`,
|
|
2937
|
+
* NOT from `is_applied`: a free product tier is only "applied" once a listed product is
|
|
2938
|
+
* in the cart, so gating "Add" on it would never let the customer add the first one.
|
|
2939
|
+
*/
|
|
2940
|
+
getPickerState(stage) {
|
|
2941
|
+
const allFreeLines = this.cartLines.filter(line => line.freeQuantity > 0);
|
|
2942
|
+
const freeLines = this.selectOwnFreeLines(allFreeLines);
|
|
2943
|
+
const free = new Set(freeLines.map(line => line.productId).filter(id => id !== null));
|
|
2944
|
+
const limit = Math.max(1, Math.floor(Number(stage.max_free_products) || 1));
|
|
2945
|
+
// The engine's answer when the server sends it; `current_value >= min_spend` is only a
|
|
2946
|
+
// fallback — it is gross of earlier discounts, so it can say "choose" while the engine
|
|
2947
|
+
// would still charge for the product.
|
|
2948
|
+
const value = Number(stage.current_value ?? this.offer?.details?.current_value ?? 0);
|
|
2949
|
+
const reached = typeof stage.can_add_free === 'boolean'
|
|
2950
|
+
? stage.can_add_free
|
|
2951
|
+
: Number.isFinite(value) && value >= Number(stage.min_spend);
|
|
2952
|
+
return { limit, reached, freeCount: free.size };
|
|
2953
|
+
}
|
|
2954
|
+
/**
|
|
2955
|
+
* The free lines this offer granted — the SAME rule as `selectOwnGifts`, so the picker and
|
|
2956
|
+
* the gifts card never disagree: lines matching the offer id; only when none match, the
|
|
2957
|
+
* lines whose `offer_id` is unreadable.
|
|
2958
|
+
*/
|
|
2959
|
+
selectOwnFreeLines(lines) {
|
|
2960
|
+
const offerId = Number(this.offer?.id);
|
|
2961
|
+
if (!Number.isFinite(offerId))
|
|
2962
|
+
return lines;
|
|
2963
|
+
const owned = lines.filter(line => line.offerId !== null && Number(line.offerId) === offerId);
|
|
2964
|
+
return owned.length ? owned : lines.filter(line => line.offerId === null);
|
|
2965
|
+
}
|
|
2966
|
+
/**
|
|
2967
|
+
* Add a free product to the cart — always from the cart page, never a redirect.
|
|
2968
|
+
*
|
|
2969
|
+
* Quick-add, exactly like the single free product was auto-added before CP-2171: a variant
|
|
2970
|
+
* (SKU) product gets its default SKU — `is_default` first, or the first in stock when stock
|
|
2971
|
+
* is per SKU (`AddToCart::addItemToCart()`) — with that SKU's option values. Any listed
|
|
2972
|
+
* product in the cart counts, so the engine then frees it. A failed add is left to the SDK,
|
|
2973
|
+
* which already shows the shopper its error notice.
|
|
2974
|
+
*/
|
|
2975
|
+
addFreeProduct(card) {
|
|
2976
|
+
if (this.pendingFreeProduct)
|
|
2977
|
+
return;
|
|
2978
|
+
this.pendingFreeProduct = card.id;
|
|
2979
|
+
this.runFreeProductChange(() => Promise.resolve(salla.cart.addItem(card.id)).then(() => { this.cartPageCheckAfter = this.giftsGeneration; }), 'free product add failed');
|
|
2980
|
+
}
|
|
2981
|
+
/** Its own method so specs can observe it; the mock window cannot reload. */
|
|
2982
|
+
reloadPage() {
|
|
2983
|
+
window.location.reload();
|
|
2984
|
+
}
|
|
2985
|
+
/**
|
|
2986
|
+
* Its own method so specs can stub it; the mock window has no `fetch`. Aborted after
|
|
2987
|
+
* `CART_PAGE_FETCH_TIMEOUT_MS`: a fetch that never settles would hold `insertingCartLines`
|
|
2988
|
+
* for the page view, and every later line would only be queued behind it.
|
|
2989
|
+
*/
|
|
2990
|
+
fetchCartPageHtml() {
|
|
2991
|
+
const controller = new AbortController();
|
|
2992
|
+
const timer = setTimeout(() => controller.abort(), CART_PAGE_FETCH_TIMEOUT_MS);
|
|
2993
|
+
// `no-store`: a cached copy of the page would lack the new line and force the reload.
|
|
2994
|
+
return fetch(window.location.href, { cache: 'no-store', credentials: 'same-origin', headers: { Accept: 'text/html' }, signal: controller.signal })
|
|
2995
|
+
.then(response => (response.ok ? response.text() : Promise.reject(new Error(`cart page ${response.status}`))))
|
|
2996
|
+
.finally(() => clearTimeout(timer));
|
|
2997
|
+
}
|
|
2998
|
+
/**
|
|
2999
|
+
* Show the cart lines an add from the picker created that the page has no `form#item-<id>`
|
|
3000
|
+
* for. Themes render cart lines on the server and only refresh lines already on the page, so
|
|
3001
|
+
* the new line (and its price) would otherwise appear only after a manual refresh.
|
|
3002
|
+
*
|
|
3003
|
+
* The cart page is fetched again and the missing line forms are inserted, in the server's
|
|
3004
|
+
* order — no reload. The fetched markup is fresh, so its prices need no update. Its inline
|
|
3005
|
+
* handlers and web components work as on a loaded page; only what the theme's own script wires
|
|
3006
|
+
* to a line at page load (e.g. raed's loading overlay on a quantity change) is missing.
|
|
3007
|
+
* Any failure (fetch, a line the fetched page lacks) falls back to `reloadForMissingCartLines`.
|
|
3008
|
+
*
|
|
3009
|
+
* Left alone on a page with no cart line forms at all (not a cart page this can reason about).
|
|
3010
|
+
* One fetch at a time; lines read meanwhile are checked again after it.
|
|
3011
|
+
*/
|
|
3012
|
+
showMissingCartLines(lines) {
|
|
3013
|
+
if (this.insertingCartLines) {
|
|
3014
|
+
this.pendingCartPageLines = lines;
|
|
3015
|
+
return;
|
|
3016
|
+
}
|
|
3017
|
+
const missing = this.missingCartLineIds(lines);
|
|
3018
|
+
if (!missing.length)
|
|
3019
|
+
return;
|
|
3020
|
+
this.insertingCartLines = true;
|
|
3021
|
+
// Started inside the chain, so a synchronous throw (no `fetch`) still reaches `catch` and `finally`.
|
|
3022
|
+
Promise.resolve()
|
|
3023
|
+
.then(() => this.fetchCartPageHtml())
|
|
3024
|
+
.then(html => {
|
|
3025
|
+
if (this.isDisconnected || this.cartChangedDuringPageFetch())
|
|
3026
|
+
return;
|
|
3027
|
+
const stillMissing = this.missingCartLineIds(this.cartLines);
|
|
3028
|
+
if (stillMissing.length && !this.insertCartLineForms(html, stillMissing)) {
|
|
3029
|
+
this.reloadForMissingCartLines(stillMissing);
|
|
3030
|
+
}
|
|
3031
|
+
})
|
|
3032
|
+
.catch(error => {
|
|
3033
|
+
this.safeLog('cart page fetch for the new lines failed, reloading', error);
|
|
3034
|
+
if (this.isDisconnected || this.cartChangedDuringPageFetch())
|
|
3035
|
+
return;
|
|
3036
|
+
this.reloadForMissingCartLines(this.missingCartLineIds(this.cartLines));
|
|
3037
|
+
})
|
|
3038
|
+
.finally(() => {
|
|
3039
|
+
this.insertingCartLines = false;
|
|
3040
|
+
const pending = this.pendingCartPageLines;
|
|
3041
|
+
this.pendingCartPageLines = null;
|
|
3042
|
+
if (pending && !this.isDisconnected)
|
|
3043
|
+
this.showMissingCartLines(pending);
|
|
3044
|
+
});
|
|
3045
|
+
}
|
|
3046
|
+
/**
|
|
3047
|
+
* A picker change started while the cart page was fetched (e.g. a second add),
|
|
3048
|
+
* so neither the fetched page nor the cart lines can be trusted: inserting now could bring
|
|
3049
|
+
* back a line the cart no longer has. The answer is dropped and the cart read after that
|
|
3050
|
+
* change checks the page again (the change itself arms `cartPageCheckAfter` when it lands;
|
|
3051
|
+
* armed here too, for a change that fails and so arms nothing).
|
|
3052
|
+
*
|
|
3053
|
+
* Lines are always read from `cartLines` once the page arrives, never from the call's own
|
|
3054
|
+
* `lines`: a change that landed during the fetch has already replaced them there.
|
|
3055
|
+
*/
|
|
3056
|
+
cartChangedDuringPageFetch() {
|
|
3057
|
+
// Lines not read since a change (the lock timed out on them) are no basis either.
|
|
3058
|
+
if (this.pendingFreeProduct === null && this.cartLinesLoaded)
|
|
3059
|
+
return false;
|
|
3060
|
+
if (this.cartPageCheckAfter === null)
|
|
3061
|
+
this.cartPageCheckAfter = this.giftsGeneration;
|
|
3062
|
+
return true;
|
|
3063
|
+
}
|
|
3064
|
+
/** Ids of `lines` the page has no form for, sorted; empty on a page with no cart line forms. */
|
|
3065
|
+
missingCartLineIds(lines) {
|
|
3066
|
+
try {
|
|
3067
|
+
if (!document.querySelector(CART_LINE_FORMS))
|
|
3068
|
+
return [];
|
|
3069
|
+
return lines.map(line => line.id).filter(id => !document.getElementById(`item-${id}`)).sort();
|
|
3070
|
+
}
|
|
3071
|
+
catch (error) {
|
|
3072
|
+
this.safeLog('cart page check after a free product add failed', error);
|
|
3073
|
+
return [];
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
/**
|
|
3077
|
+
* Insert the `missing` line forms from the fetched cart page `html`, each right before the next
|
|
3078
|
+
* line the page already has (in the fetched order), else after the last one. Returns false —
|
|
3079
|
+
* nothing inserted — when the fetched page lacks any of them.
|
|
3080
|
+
*/
|
|
3081
|
+
insertCartLineForms(html, missing) {
|
|
3082
|
+
try {
|
|
3083
|
+
const fetched = Array.from(new DOMParser().parseFromString(html, 'text/html').querySelectorAll(CART_LINE_FORMS));
|
|
3084
|
+
// `getAttribute`, not `form.id`: a line form holds an `<input name="id">`, which shadows the property.
|
|
3085
|
+
const fetchedIds = fetched.map(form => form.getAttribute('id'));
|
|
3086
|
+
if (missing.some(id => !fetchedIds.includes(`item-${id}`)))
|
|
3087
|
+
return false;
|
|
3088
|
+
fetched.forEach((form, index) => {
|
|
3089
|
+
const id = fetchedIds[index];
|
|
3090
|
+
if (!missing.includes(id.slice('item-'.length)) || document.getElementById(id))
|
|
3091
|
+
return;
|
|
3092
|
+
// Moved, not cloned: inserting adopts it into this document, and its web components
|
|
3093
|
+
// upgrade once connected. Its inline handlers (`onchange`, delete `onclick`) come along.
|
|
3094
|
+
const next = fetchedIds.slice(index + 1).map(nextId => document.getElementById(nextId)).find(Boolean);
|
|
3095
|
+
if (next) {
|
|
3096
|
+
next.parentNode.insertBefore(form, next);
|
|
3097
|
+
}
|
|
3098
|
+
else {
|
|
3099
|
+
const onPage = document.querySelectorAll(CART_LINE_FORMS);
|
|
3100
|
+
const last = onPage[onPage.length - 1];
|
|
3101
|
+
last.parentNode.insertBefore(form, last.nextSibling);
|
|
3102
|
+
}
|
|
3103
|
+
});
|
|
3104
|
+
return missing.every(id => document.getElementById(`item-${id}`));
|
|
3105
|
+
}
|
|
3106
|
+
catch (error) {
|
|
3107
|
+
this.safeLog('cart line insert failed', error);
|
|
3108
|
+
return false;
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
/**
|
|
3112
|
+
* Reload once, when the missing lines could not be inserted. Never twice for the same missing
|
|
3113
|
+
* lines, so a line a theme never renders cannot loop the page. The guard lives in
|
|
3114
|
+
* `sessionStorage`; where storage is blocked the page still reloads, but never twice in a row.
|
|
3115
|
+
*/
|
|
3116
|
+
reloadForMissingCartLines(missing) {
|
|
3117
|
+
if (!missing.length)
|
|
3118
|
+
return;
|
|
3119
|
+
const signature = missing.join(',');
|
|
3120
|
+
const stored = this.readReloadGuard();
|
|
3121
|
+
if (stored === signature)
|
|
3122
|
+
return;
|
|
3123
|
+
if (stored === undefined && this.pageWasReloaded())
|
|
3124
|
+
return;
|
|
3125
|
+
this.writeReloadGuard(signature);
|
|
3126
|
+
this.reloadPage();
|
|
3127
|
+
}
|
|
3128
|
+
/** The reload guard, or `undefined` when storage cannot be read (blocked, disabled). */
|
|
3129
|
+
readReloadGuard() {
|
|
3130
|
+
try {
|
|
3131
|
+
return window.sessionStorage.getItem(CART_PAGE_RELOAD_KEY);
|
|
3132
|
+
}
|
|
3133
|
+
catch {
|
|
3134
|
+
return undefined;
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
writeReloadGuard(signature) {
|
|
3138
|
+
try {
|
|
3139
|
+
window.sessionStorage.setItem(CART_PAGE_RELOAD_KEY, signature);
|
|
3140
|
+
}
|
|
3141
|
+
catch {
|
|
3142
|
+
// Storage full or blocked: pageWasReloaded() is the guard on the next load.
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
/** Whether this page load is itself a reload — the guard when storage is unavailable. */
|
|
3146
|
+
pageWasReloaded() {
|
|
3147
|
+
try {
|
|
3148
|
+
const navigation = window.performance?.getEntriesByType?.('navigation')?.[0];
|
|
3149
|
+
return navigation?.type === 'reload';
|
|
3150
|
+
}
|
|
3151
|
+
catch {
|
|
3152
|
+
return false;
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
/**
|
|
3156
|
+
* Run one add. A failure unlocks at once — nothing changed. A success stays locked
|
|
3157
|
+
* until a cart load that started after it publishes (see `loadCart`), or the fallback fires.
|
|
3158
|
+
*/
|
|
3159
|
+
runFreeProductChange(change, failure) {
|
|
3160
|
+
Promise.resolve()
|
|
3161
|
+
.then(change)
|
|
3162
|
+
.then(() => {
|
|
3163
|
+
if (this.isDisconnected)
|
|
3164
|
+
return this.unlockFreeProduct();
|
|
3165
|
+
this.pendingFreeProductSettledAt = this.giftsGeneration;
|
|
3166
|
+
this.pendingFreeProductTimer = setTimeout(() => this.unlockOnStaleLines(), FREE_PRODUCT_UNLOCK_FALLBACK_MS);
|
|
3167
|
+
})
|
|
3168
|
+
.catch(error => {
|
|
3169
|
+
this.safeLog(failure, error);
|
|
3170
|
+
this.unlockFreeProduct();
|
|
3171
|
+
// A failed change fires no cart event, so nothing else checks a cart page answer
|
|
3172
|
+
// that was dropped while this change was in flight.
|
|
3173
|
+
if (this.cartPageCheckAfter !== null)
|
|
3174
|
+
this.loadCart();
|
|
3175
|
+
});
|
|
3176
|
+
}
|
|
3177
|
+
/**
|
|
3178
|
+
* The lock's fallback: no cart lines read after the change have arrived, so the lines on
|
|
3179
|
+
* screen are the ones from BEFORE it. Release the lock, mark the lines unread and ask again
|
|
3180
|
+
* on a fresh read — `loadCart` retries within its bound while the picker has no lines.
|
|
3181
|
+
*/
|
|
3182
|
+
unlockOnStaleLines() {
|
|
3183
|
+
this.unlockFreeProduct();
|
|
3184
|
+
this.cartLinesLoaded = false;
|
|
3185
|
+
this.clearPickerLookupRetry();
|
|
3186
|
+
refreshCartFreeGifts().then(() => this.loadCart(), () => this.loadCart());
|
|
3187
|
+
}
|
|
3188
|
+
unlockFreeProduct() {
|
|
3189
|
+
if (this.pendingFreeProductTimer !== null)
|
|
3190
|
+
clearTimeout(this.pendingFreeProductTimer);
|
|
3191
|
+
this.pendingFreeProductTimer = null;
|
|
3192
|
+
this.pendingFreeProductSettledAt = null;
|
|
3193
|
+
this.pendingFreeProduct = null;
|
|
3194
|
+
}
|
|
3195
|
+
/**
|
|
3196
|
+
* CP-2171 — Figma's banner line at the gift tier: "You got N free products", with
|
|
3197
|
+
* "Choose them now" while none is picked yet. N is the tier's limit, not the count picked.
|
|
3198
|
+
*/
|
|
3199
|
+
pickerGotMessage(limit, awaitsPick) {
|
|
3200
|
+
if (limit === 1)
|
|
3201
|
+
return this.trans(awaitsPick ? 'free_product_got_pick_one' : 'free_product_got_one');
|
|
3202
|
+
if (limit === 2)
|
|
3203
|
+
return this.trans(awaitsPick ? 'free_products_got_pick_two' : 'free_products_got_two');
|
|
3204
|
+
return this.trans(awaitsPick ? 'free_products_got_pick' : 'free_products_got', { count: `${limit}` });
|
|
3205
|
+
}
|
|
3206
|
+
getPickerCards(stage) {
|
|
3207
|
+
return (stage.free_products ?? [])
|
|
3208
|
+
.map(ref => this.freeProductCards[String(ref.id)])
|
|
3209
|
+
.filter(Boolean);
|
|
3210
|
+
}
|
|
3211
|
+
/** CP-2171 — Figma "Choose N free products": the counter, then one card per listed product. */
|
|
3212
|
+
renderPicker(stage) {
|
|
3213
|
+
const state = this.getPickerState(stage);
|
|
3214
|
+
const cards = this.getPickerCards(stage);
|
|
3215
|
+
// Cards still loading, or the lookup failed: no header and counter over an empty list.
|
|
3216
|
+
if (!cards.length)
|
|
3217
|
+
return null;
|
|
3218
|
+
return (index.h("div", { class: "s-conditional-offer-picker", "data-picker-reached": state.reached ? 'true' : 'false' }, index.h("div", { class: "s-conditional-offer-picker-header" }, index.h("span", { class: "s-conditional-offer-picker-counter", "aria-label": `${state.freeCount}/${state.limit}` }, `${state.freeCount}/${state.limit}`), index.h("span", { class: "s-conditional-offer-picker-title" }, state.limit === 1
|
|
3219
|
+
? this.trans('free_product_choose_one')
|
|
3220
|
+
: state.limit === 2
|
|
3221
|
+
? this.trans('free_products_choose_two')
|
|
3222
|
+
: this.trans('free_products_choose', { count: `${state.limit}` }))), index.h("ul", { class: "s-conditional-offer-picker-list" }, cards.map(card => this.renderPickerCard(card)))));
|
|
3223
|
+
}
|
|
3224
|
+
/**
|
|
3225
|
+
* One picker card: the product, its normal price and Add — the same on every card, whether
|
|
3226
|
+
* the product is in the cart, free or not. Add is never disabled: every tap adds one unit at
|
|
3227
|
+
* the normal price (also once the product is in the cart, and after the free limit is used
|
|
3228
|
+
* up) and the engine decides what is free. What IS free shows in the "Free products" panel
|
|
3229
|
+
* below, never on the card. A tap while the previous add is still in flight is ignored
|
|
3230
|
+
* (`addFreeProduct`), so one double tap is one unit.
|
|
3231
|
+
*
|
|
3232
|
+
* No remove: the picker never deletes a cart line. A free line can also hold paid units, and
|
|
3233
|
+
* deleting it would take what the customer bought with the gift — products leave the cart
|
|
3234
|
+
* from their own cart line only.
|
|
3235
|
+
*/
|
|
3236
|
+
renderPickerCard(card) {
|
|
3237
|
+
return (index.h("li", { class: "s-conditional-offer-picker-card", "data-product-id": card.id }, index.h("span", { class: "s-conditional-offer-picker-thumb" }, card.imageUrl ? (index.h("img", { class: "s-conditional-offer-picker-image", src: card.imageUrl, alt: card.imageAlt || card.name, loading: "lazy", decoding: "async" })) : (index.h("i", { class: "sicon-gift", "aria-hidden": "true" }))), index.h("span", { class: "s-conditional-offer-picker-name" }, card.name), card.price !== null ? (index.h("span", { class: "s-conditional-offer-picker-prices" }, index.h("span", { class: "s-conditional-offer-picker-price-regular", innerHTML: salla.money(card.price) }))) : null, index.h("span", { class: "s-conditional-offer-picker-actions" }, index.h("button", { type: "button", class: "s-conditional-offer-picker-add", onClick: () => this.addFreeProduct(card) }, this.getAddLabel()))));
|
|
3238
|
+
}
|
|
2654
3239
|
/**
|
|
2655
3240
|
* "ادارة العرض". Rendered TWICE with complementary responsive visibility rather than
|
|
2656
3241
|
* moved with `order:`: desktop wants it inline with the banner row, mobile wants a
|
|
2657
3242
|
* full-width bar BELOW the gifts card, and those are different places
|
|
2658
3243
|
* in the flow, not a different order within one flex line.
|
|
2659
3244
|
*/
|
|
2660
|
-
renderGiftsToggle(variant) {
|
|
3245
|
+
renderGiftsToggle(variant, hasPicker) {
|
|
2661
3246
|
return (index.h("button", { type: "button", id: this.elementId(`toggle-${variant}`), class: {
|
|
2662
3247
|
's-conditional-offer-toggle': true,
|
|
2663
3248
|
's-conditional-offer-toggle-inline': variant === 'inline',
|
|
2664
3249
|
's-conditional-offer-toggle-bar': variant === 'bar',
|
|
2665
|
-
}, "aria-expanded": this.isGiftsOpen ? 'true' : 'false', "aria-controls": this.elementId('gifts'), onClick: () => (this.isGiftsOpen = !this.isGiftsOpen) }, index.h("span", null, this.trans('manage_offer')), index.h("i", { class: {
|
|
3250
|
+
}, "aria-expanded": this.isGiftsOpen ? 'true' : 'false', "aria-controls": this.elementId('gifts'), onClick: () => (this.isGiftsOpen = !this.isGiftsOpen) }, index.h("span", null, this.trans(hasPicker ? 'offer_toggle' : 'manage_offer')), index.h("i", { class: {
|
|
2666
3251
|
'sicon-keyboard_arrow_down': true,
|
|
2667
3252
|
's-conditional-offer-toggle-icon': true,
|
|
2668
3253
|
's-conditional-offer-toggle-icon-open': this.isGiftsOpen,
|
|
2669
3254
|
}, "aria-hidden": "true" })));
|
|
2670
3255
|
}
|
|
2671
|
-
/**
|
|
2672
|
-
|
|
2673
|
-
|
|
3256
|
+
/**
|
|
3257
|
+
* Spec §5 — the gifts card: one row per gift line. Read-only on an offer without a picker,
|
|
3258
|
+
* exactly as before; with the CP-2171 picker it becomes the Figma "Free products" panel,
|
|
3259
|
+
* shown only once a gift is in the cart. Its rows are read-only, as are the picker cards: a
|
|
3260
|
+
* gift leaves the cart from its own cart line.
|
|
3261
|
+
*/
|
|
3262
|
+
renderGiftsCard(pickerStage = null) {
|
|
3263
|
+
// No picker: the card exactly as before CP-2171 — no wrapper, so no new box styling.
|
|
3264
|
+
if (!pickerStage) {
|
|
3265
|
+
return (index.h("div", { class: "s-conditional-offer-panel", id: this.elementId('gifts') }, index.h("div", { class: "s-conditional-offer-gifts-header" }, index.h("span", { class: "s-conditional-offer-gifts-icon" }, index.h("i", { class: "sicon-gift", "aria-hidden": "true" })), index.h("span", { class: "s-conditional-offer-gifts-title" }, this.trans('free_products_title'))), index.h("ul", { class: "s-conditional-offer-gifts-list" }, this.gifts.map(gift => this.renderGiftRow(gift)))));
|
|
3266
|
+
}
|
|
3267
|
+
const state = this.getPickerState(pickerStage);
|
|
3268
|
+
return (index.h("div", { class: "s-conditional-offer-panel", id: this.elementId('gifts') }, state.reached ? this.renderPicker(pickerStage) : null, this.gifts.length ? (index.h("div", { class: "s-conditional-offer-gifts" }, index.h("div", { class: "s-conditional-offer-gifts-header" }, index.h("span", { class: "s-conditional-offer-gifts-icon" }, index.h("i", { class: "sicon-gift", "aria-hidden": "true" })), index.h("span", { class: "s-conditional-offer-gifts-title" }, this.sharedLabel(FREE_PRODUCTS_TITLE_KEY, FREE_PRODUCTS_TITLE_FALLBACK))), index.h("ul", { class: "s-conditional-offer-gifts-list" }, this.gifts.map(gift => this.renderGiftRow(gift))))) : null));
|
|
2674
3269
|
}
|
|
2675
3270
|
renderGiftRow(gift) {
|
|
2676
3271
|
return (index.h("li", { class: "s-conditional-offer-gift", "data-item-id": gift.id }, index.h("span", { class: "s-conditional-offer-gift-thumb" }, gift.imageUrl ? (index.h("img", { class: "s-conditional-offer-gift-image", src: gift.imageUrl, alt: gift.imageAlt || gift.name, loading: "lazy", decoding: "async" })) : (index.h("i", { class: "sicon-gift", "aria-hidden": "true" }))), index.h("div", { class: "s-conditional-offer-gift-body" }, index.h("span", { class: "s-conditional-offer-gift-name" }, gift.name || this.trans('free_gift')), index.h("span", { class: "s-conditional-offer-gift-prices" }, index.h("span", { class: "s-conditional-offer-gift-free" }, this.getFreeLabel()), gift.originalPrice !== null ? (
|
|
@@ -2717,12 +3312,26 @@ const SallaConditionalOffer = class {
|
|
|
2717
3312
|
const progress = this.getProgress(stages);
|
|
2718
3313
|
const guidance = this.getGuidanceMessage(stages, progress);
|
|
2719
3314
|
const detailsTitle = this.trans('details_title');
|
|
2720
|
-
// Spec §2 — the accordion exists only when there is something to manage
|
|
2721
|
-
|
|
2722
|
-
|
|
3315
|
+
// Spec §2 — the accordion exists only when there is something to manage: gift lines,
|
|
3316
|
+
// or (CP-2171) a free product picker — shown only once the customer has reached its
|
|
3317
|
+
// stage, then before anything is picked too.
|
|
3318
|
+
const pickerStage = this.getPickerStage(stages);
|
|
3319
|
+
const pickerState = pickerStage !== null ? this.getPickerState(pickerStage) : null;
|
|
3320
|
+
const isPickerReached = pickerState?.reached ?? false;
|
|
3321
|
+
const hasGifts = this.gifts.length > 0 || (isPickerReached && this.getPickerCards(pickerStage).length > 0);
|
|
3322
|
+
// The gift tier is reached but none of this offer's gifts is in the cart: the engine
|
|
3323
|
+
// grants that tier only once one is, and pays nothing until then (no fallback to a lower
|
|
3324
|
+
// tier). So neither "congrats" nor "conditions unmet" is true — ask for the pick. Only
|
|
3325
|
+
// once the cart lines are read, or a cart that holds the gift would flash this line.
|
|
3326
|
+
const awaitsGiftPick = isPickerReached && this.cartLinesLoaded && pickerState.freeCount === 0;
|
|
3327
|
+
// "You got N free products" replaces congrats only when the gift tier is the offer's last:
|
|
3328
|
+
// below a higher tier, `guidance` (or congrats for that higher tier) is the truer line.
|
|
3329
|
+
const gotGifts = progress.allMet && isPickerReached && pickerState.freeCount > 0
|
|
3330
|
+
&& stages[stages.length - 1] === pickerStage;
|
|
3331
|
+
return (index.h(index.Host, { class: "s-conditional-offer-root" }, index.h("div", { class: "s-conditional-offer-banner" }, index.h("div", { class: "s-conditional-offer-lead" }, this.renderBadges(stages, progress), index.h("div", { class: "s-conditional-offer-text" }, index.h("div", { class: "s-conditional-offer-heading", onMouseEnter: () => (this.isTooltipHovered = true), onMouseLeave: () => (this.isTooltipHovered = false) }, index.h("span", { class: "s-conditional-offer-name" }, this.offer.title), index.h("span", { class: "s-conditional-offer-info", ref: (el) => (this.infoAnchorEl = el) }, index.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() }, index.h(InformationIcon, { class: "s-conditional-offer-info-icon" })), this.isTooltipOpen ? this.renderTooltip(stages, progress) : null)), awaitsGiftPick || gotGifts ? (index.h("div", { class: "s-conditional-offer-message", role: "status" }, this.pickerGotMessage(pickerState.limit, awaitsGiftPick))) : this.areGiftsUnavailable(stages, progress) ? (index.h("div", { class: "s-conditional-offer-message", role: "status" }, this.trans('free_products_unavailable'))) : progress.allMet ? (index.h("div", { class: "s-conditional-offer-message", role: "status" }, this.trans('congrats'))) : guidance ? (
|
|
2723
3332
|
// innerHTML: the `:remaining` half carries `salla.money()` markup;
|
|
2724
3333
|
// the reward half is escaped in `getGuidanceMessage`.
|
|
2725
|
-
index.h("div", { class: "s-conditional-offer-message", innerHTML: guidance })) : null)), hasGifts ? this.renderGiftsToggle('inline') : null), hasGifts && this.isGiftsOpen ? this.renderGiftsCard() : null, hasGifts ? this.renderGiftsToggle('bar') : null));
|
|
3334
|
+
index.h("div", { class: "s-conditional-offer-message", innerHTML: guidance })) : null)), hasGifts ? this.renderGiftsToggle('inline', !!pickerStage) : null), hasGifts && this.isGiftsOpen ? this.renderGiftsCard(pickerStage) : null, hasGifts ? this.renderGiftsToggle('bar', !!pickerStage) : null));
|
|
2726
3335
|
}
|
|
2727
3336
|
get hostEl() { return index.getElement(this); }
|
|
2728
3337
|
};
|