UNPKG

@salla.sa/twilight-components

Version:
252 lines (247 loc) 14.1 kB
/*! * Crafted with ❤ by Salla */ 'use strict'; var index = require('./index-C7gO-zm5.js'); const sallaCartCouponsCss = ""; const SallaCartCoupons = class { constructor(hostRef) { index.registerInstance(this, hostRef); this.availableCoupons = []; this.cartTotal = 0; this.currentCoupon = ''; this.couponInputValue = ''; this.couponError = ''; this.isApplying = false; this.isHandlingInternalError = false; // Store cart event listener references for cleanup this.cartUpdatedHandler = (cartData) => { this.cartTotal = cartData.sub_total ?? cartData.total ?? 0; // Update current coupon state const couponCode = cartData.coupon || ''; // Only overwrite the user's typed value when the cart-level coupon itself changes if (couponCode !== this.currentCoupon) { this.currentCoupon = couponCode; this.couponInputValue = couponCode; } // Clear error when cart updates this.couponError = ''; }; this.couponAddedHandler = () => { // Coupon was successfully added - state is managed by cartUpdatedHandler }; this.couponAdditionFailedHandler = (error) => { // Only handle external coupon failures when no internal operation is handling errors if (!this.isApplying && !this.isHandlingInternalError) { this.showCouponError(error?.message || salla.lang.get('pages.cart.coupon_application_error')); } // Internal operations handle their own errors to prevent double display }; this.couponDeletedHandler = () => { // Coupon was removed - state is managed by cartUpdatedHandler }; // Arrow functions to maintain context for event listeners this.onKeyUp = (event) => { if (event.key === 'Enter') { this.couponButtonRef?.click(); } this.couponError = ''; }; this.onInput = (event) => { this.couponInputValue = event.target.value; }; } async componentWillLoad() { await Salla.onReady(); await Salla.lang.onLoaded(); //then set your logic here to avoid any race condition this.loadCouponsData(); // Load async without blocking render - shows empty until loaded this.loadCartTotal(); this.setupCartEventListeners(); // Note: No store feature gate needed - coupon availability controlled by backend API response } componentDidLoad() { this.setupCouponInputEvents(); } setupCartEventListeners() { // Listen for cart updates to refresh cart total salla.event.on('cart::updated', this.cartUpdatedHandler); // Listen for coupon events salla.event.on('cart::coupon.added', this.couponAddedHandler); salla.event.on('cart::coupon.addition.failed', this.couponAdditionFailedHandler); salla.event.on('cart::coupon.deleted', this.couponDeletedHandler); } disconnectedCallback() { // Clean up DOM event listeners this.couponInputRef?.removeEventListener('keyup', this.onKeyUp); this.couponInputRef?.removeEventListener('input', this.onInput); // Clean up cart event listeners using reliable global event system salla.event.off('cart::updated', this.cartUpdatedHandler); salla.event.off('cart::coupon.added', this.couponAddedHandler); salla.event.off('cart::coupon.addition.failed', this.couponAdditionFailedHandler); salla.event.off('cart::coupon.deleted', this.couponDeletedHandler); } /** * Business Decision: No visual selected state needed * UX Flow: Click coupon → populate input → auto-apply (streamlined experience) */ async selectCoupon(couponCode) { const selectedCoupon = this.availableCoupons.find(coupon => coupon.code === couponCode); if (selectedCoupon) { if (!this.isEligible(selectedCoupon)) { throw new Error(`Coupon not eligible: minimum cart amount not met`); } try { // Actually apply the coupon to the cart first await this.applyCoupon(couponCode); // Only dispatch event after successful API call this.host.dispatchEvent(new CustomEvent('salla::cart-coupons.coupon.selected', { detail: selectedCoupon, bubbles: true })); } catch (error) { // Rethrow to let caller handle the error throw error; } } return selectedCoupon; } async loadCouponsData() { try { // Load coupons from API const response = await salla.api.request('coupons'); const raw = response?.data; const coupons = Array.isArray(raw) ? raw : (Array.isArray(raw?.data) ? raw.data : []); this.availableCoupons = coupons; } catch (error) { console.warn('Failed to load coupons:', error); // No coupons available on error this.availableCoupons = []; } } loadCartTotal() { // Get cart total and current coupon from Salla storage const cart = salla.storage.get('cart'); // Check summary first (more accurate), then cart level, fallback to 0 this.cartTotal = cart?.summary?.sub_total ?? cart?.sub_total ?? 0; this.currentCoupon = cart?.coupon || ''; this.couponInputValue = cart?.coupon || ''; } setupCouponInputEvents() { if (!this.couponInputRef || !this.couponButtonRef) { return; } this.couponInputRef.addEventListener('keyup', this.onKeyUp); this.couponInputRef.addEventListener('input', this.onInput); } async handleCouponButtonClick() { if (this.isApplying) { return; } const hasCoupon = !!this.currentCoupon; if (!hasCoupon && !this.couponInputValue.trim()) { this.showCouponError(salla.lang.get('pages.cart.enter_coupon_code')); return; } this.isApplying = true; this.isHandlingInternalError = true; try { if (hasCoupon) { // Remove existing coupon await salla.cart.deleteCoupon(); } else { // Apply new coupon await salla.cart.addCoupon(this.couponInputValue.trim()); } } catch (error) { this.showCouponError(error.response?.data?.error?.message || salla.lang.get('pages.cart.coupon_application_error')); } finally { this.isApplying = false; // Keep internal error flag active briefly to prevent race condition with SDK event setTimeout(() => { this.isHandlingInternalError = false; }, 100); } } showCouponError(message) { this.couponError = message; } handleCouponClick(coupon) { this.applyCoupon(coupon.code, false).catch(() => { // Error already displayed by applyCoupon }); } async applyCoupon(couponCode, shouldThrow = true) { if (this.isApplying) { if (shouldThrow) { throw new Error('Coupon application already in progress'); } else { return; // Silent return for card clicks when operation is already in progress } } // Update the input field with the coupon code this.couponInputValue = couponCode; // Clear any existing errors this.couponError = ''; this.isApplying = true; this.isHandlingInternalError = true; try { // Apply the coupon directly without checking for existing coupon await salla.cart.addCoupon(couponCode.trim()); } catch (error) { this.showCouponError(error.response?.data?.error?.message || salla.lang.get('pages.cart.coupon_application_error')); // Re-throw error only if requested (for selectCoupon API) if (shouldThrow) { throw error; } } finally { this.isApplying = false; // Keep internal error flag active briefly to prevent race condition with SDK event setTimeout(() => { this.isHandlingInternalError = false; }, 100); } } formatMainAmount(coupon) { if (coupon.type === 'percentage') { // For percentage coupons, amount should be a string or number const percentageValue = typeof coupon.amount === 'object' ? coupon.amount.amount : coupon.amount; return `${salla.helpers.number(percentageValue)}%`; } else { // For fixed coupons, amount is an object with amount and currency const amountData = typeof coupon.amount === 'object' ? coupon.amount : { amount: coupon.amount, currency: coupon.minimum_amount?.currency || salla.config.get('store.currency.code', 'SAR') }; return (index.h("span", { innerHTML: salla.money(amountData.amount, amountData.currency) })); } } isEligible(coupon) { if (coupon.minimum_amount && this.cartTotal < parseFloat(coupon.minimum_amount.amount)) { return false; // Coupon is not eligible due to minimum amount requirement } return true; } renderCouponCard(coupon) { const isEligible = this.isEligible(coupon); // Note: Card notches (ticket-style pseudo-elements) match card background, not page background - this is intentional design return (index.h("div", { class: "s-cart-coupons-slide-one-fourth swiper-slide" }, index.h("div", { class: `s-cart-coupons-card ${!isEligible || this.isApplying ? 's-cart-coupons-disabled' : ''}`, onClick: () => isEligible && !this.isApplying && this.handleCouponClick(coupon) }, index.h("div", { class: "s-cart-coupons-top-section" }, index.h("div", { class: "s-cart-coupons-discount-values" }, index.h("div", { class: "s-cart-coupons-main-amount" }, this.formatMainAmount(coupon)), coupon.free_shipping ? (index.h("div", { class: "s-cart-coupons-secondary-info" }, "+ ", salla.lang.get('pages.cart.free_shipping'))) : coupon.maximum_amount && (index.h("div", { class: "s-cart-coupons-secondary-info" }, salla.lang.get('pages.cart.up_to'), " ", index.h("span", { innerHTML: salla.money(coupon.maximum_amount.amount, coupon.maximum_amount.currency) })))), index.h("div", { class: "s-cart-coupons-code" }, coupon.code)), index.h("div", { class: "s-cart-coupons-divider" }), index.h("div", { class: "s-cart-coupons-bottom-section" }, index.h("div", { class: "s-cart-coupons-terms" }, coupon.remaining_days !== undefined && coupon.remaining_days !== null && (index.h("div", { class: `s-cart-coupons-expiry ${coupon.remaining_days <= 3 ? 's-cart-coupons-expiry-warning' : ''}` }, coupon.remaining_days === 0 ? salla.lang.get('pages.cart.expires_today') : `${salla.lang.get('pages.cart.expires_after')} ${coupon.remaining_days} ${salla.lang.get('pages.cart.day')}`)), coupon.minimum_amount && parseFloat(coupon.minimum_amount.amount) > 0 && (index.h("div", { class: "s-cart-coupons-minimum" }, salla.lang.get('pages.cart.minimum'), " ", index.h("span", { innerHTML: salla.money(coupon.minimum_amount.amount, coupon.minimum_amount.currency) })))), index.h("div", { class: "s-cart-coupons-action" }, isEligible ? salla.lang.get('pages.cart.save_coupon') : salla.lang.get('pages.cart.coupon_not_compatible')))))); } render() { return (index.h(index.Host, { key: 'f3f3d943ab2c1a6488a1da02d2cc031dbc88b482' }, index.h("div", { key: 'abf88828017041b19a6c54ba2f99e6c246bd8d04', class: "s-cart-coupons-wrapper" }, index.h("div", { key: 'c2946579a7cca935722997229f50ede603acb8b2', class: "s-cart-coupons-input-section" }, index.h("label", { key: '50042d827c86daf853babd4e41ba3a920aceda9d', htmlFor: "coupon-input", class: "s-cart-coupons-coupon-label" }, salla.lang.get('pages.cart.have_coupon')), index.h("div", { key: '4b01047727bd3a83f6cfbac37221e7732076248f', class: "s-cart-coupons-coupon-input-container" }, index.h("input", { key: '555108f68b7a60c618d9a904905ef321cd274c7a', type: "text", id: "coupon-input", name: "coupon", class: `form-input ${this.couponError ? 's-coupon-input-error' : ''}`, placeholder: salla.lang.get('pages.cart.coupon_placeholder'), value: this.couponInputValue, disabled: !!this.currentCoupon || this.isApplying, ref: el => this.couponInputRef = el, "aria-label": salla.lang.get('pages.cart.apply_coupon') }), index.h("salla-button", { key: '0416bbaf029f7d8951ca6a419cdc676ae50ee792', ref: el => this.couponButtonRef = el, class: `s-cart-coupons-coupon-button ${this.currentCoupon ? 's-cart-coupons-coupon-button-remove' : 's-cart-coupons-coupon-button-apply'}`, color: this.currentCoupon ? 'danger' : 'primary', loading: this.isApplying, "loader-position": "center", onClick: () => this.handleCouponButtonClick() }, this.currentCoupon ? (index.h("i", { class: "sicon-cancel" })) : (index.h("span", null, salla.lang.get('pages.cart.save_coupon'))))), this.couponError && (index.h("span", { key: 'f7c77e8676cb07b4820b571dbd8ee25dca79a1b1', class: "s-cart-coupons-coupon-error" }, this.couponError))), this.availableCoupons && this.availableCoupons.length > 0 && (index.h("salla-slider", { key: 'daee9c6705de734d5f81ff86d92ad6952b5a7332', type: "carousel", showControls: false, class: "s-cart-coupons-slider" }, index.h("div", { key: '5d5ef30a6ff7f7c2b406cc5cec6622c2f028d17e', slot: "items" }, this.availableCoupons.map(coupon => this.renderCouponCard(coupon)))))))); } get host() { return index.getElement(this); } }; SallaCartCoupons.style = sallaCartCouponsCss; exports.salla_cart_coupons = SallaCartCoupons;