@salla.sa/twilight-components
Version:
Salla Web Component
288 lines (283 loc) • 15.3 kB
JavaScript
/*!
* Crafted with ❤ by Salla
*/
import { proxyCustomElement, HTMLElement, h, Host } from '@stencil/core/internal/client';
import { d as defineCustomElement$3 } from './salla-button2.js';
import { d as defineCustomElement$2 } from './salla-slider2.js';
const sallaCartCouponsCss = "";
const SallaCartCoupons$1 = /*@__PURE__*/ proxyCustomElement(class SallaCartCoupons extends HTMLElement {
constructor() {
super();
this.__registerHost();
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 (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 (h("div", { class: "s-cart-coupons-slide-one-fourth swiper-slide" }, h("div", { class: `s-cart-coupons-card ${!isEligible || this.isApplying ? 's-cart-coupons-disabled' : ''}`, onClick: () => isEligible && !this.isApplying && this.handleCouponClick(coupon) }, h("div", { class: "s-cart-coupons-top-section" }, h("div", { class: "s-cart-coupons-discount-values" }, h("div", { class: "s-cart-coupons-main-amount" }, this.formatMainAmount(coupon)), coupon.free_shipping ? (h("div", { class: "s-cart-coupons-secondary-info" }, "+ ", salla.lang.get('pages.cart.free_shipping'))) : coupon.maximum_amount && (h("div", { class: "s-cart-coupons-secondary-info" }, salla.lang.get('pages.cart.up_to'), " ", h("span", { innerHTML: salla.money(coupon.maximum_amount.amount, coupon.maximum_amount.currency) })))), h("div", { class: "s-cart-coupons-code" }, coupon.code)), h("div", { class: "s-cart-coupons-divider" }), h("div", { class: "s-cart-coupons-bottom-section" }, h("div", { class: "s-cart-coupons-terms" }, coupon.remaining_days !== undefined && coupon.remaining_days !== null && (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 && (h("div", { class: "s-cart-coupons-minimum" }, salla.lang.get('pages.cart.minimum'), " ", h("span", { innerHTML: salla.money(coupon.minimum_amount.amount, coupon.minimum_amount.currency) })))), 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 (h(Host, { key: 'f3f3d943ab2c1a6488a1da02d2cc031dbc88b482' }, h("div", { key: 'abf88828017041b19a6c54ba2f99e6c246bd8d04', class: "s-cart-coupons-wrapper" }, h("div", { key: 'c2946579a7cca935722997229f50ede603acb8b2', class: "s-cart-coupons-input-section" }, h("label", { key: '50042d827c86daf853babd4e41ba3a920aceda9d', htmlFor: "coupon-input", class: "s-cart-coupons-coupon-label" }, salla.lang.get('pages.cart.have_coupon')), h("div", { key: '4b01047727bd3a83f6cfbac37221e7732076248f', class: "s-cart-coupons-coupon-input-container" }, 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') }), 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 ? (h("i", { class: "sicon-cancel" })) : (h("span", null, salla.lang.get('pages.cart.save_coupon'))))), this.couponError && (h("span", { key: 'f7c77e8676cb07b4820b571dbd8ee25dca79a1b1', class: "s-cart-coupons-coupon-error" }, this.couponError))), this.availableCoupons && this.availableCoupons.length > 0 && (h("salla-slider", { key: 'daee9c6705de734d5f81ff86d92ad6952b5a7332', type: "carousel", showControls: false, class: "s-cart-coupons-slider" }, h("div", { key: '5d5ef30a6ff7f7c2b406cc5cec6622c2f028d17e', slot: "items" }, this.availableCoupons.map(coupon => this.renderCouponCard(coupon))))))));
}
get host() { return this; }
static get style() { return sallaCartCouponsCss; }
}, [0, "salla-cart-coupons", {
"availableCoupons": [32],
"cartTotal": [32],
"currentCoupon": [32],
"couponInputValue": [32],
"couponError": [32],
"isApplying": [32],
"selectCoupon": [64]
}]);
function defineCustomElement$1() {
if (typeof customElements === "undefined") {
return;
}
const components = ["salla-cart-coupons", "salla-button", "salla-slider"];
components.forEach(tagName => { switch (tagName) {
case "salla-cart-coupons":
if (!customElements.get(tagName)) {
customElements.define(tagName, SallaCartCoupons$1);
}
break;
case "salla-button":
if (!customElements.get(tagName)) {
defineCustomElement$3();
}
break;
case "salla-slider":
if (!customElements.get(tagName)) {
defineCustomElement$2();
}
break;
} });
}
defineCustomElement$1();
const SallaCartCoupons = SallaCartCoupons$1;
const defineCustomElement = defineCustomElement$1;
export { SallaCartCoupons, defineCustomElement };