UNPKG

@salla.sa/twilight-components

Version:
297 lines (296 loc) 16.6 kB
/*! * Crafted with ❤ by Salla */ import { h, Host } from "@stencil/core"; import { fetchCities, fetchDeliveryMessage } from "./delivery-promise-api"; import { buildBulletOpenPayload, resolveIntent } from "./delivery-promise-intent"; import { DEFAULT_LABELS, getCityDisplayName, loadLabels } from "./delivery-promise-labels"; import { log } from "./delivery-promise-api"; const OVERRIDE_IP_KEY = 'salla-bullet-delivery-override-ip'; /** * @name SallaDeliveryPromise * @description Displays delivery promise message (from BE) with city selection * @tag salla-delivery-promise */ export class SallaDeliveryPromise { constructor() { // ── Feature flags & config ──────────────────────────────────────────── this.isDeliveryPromiseEnabled = false; this.isLoginCycleEnabled = false; this.canRender = false; // ── Shared display state ────────────────────────────────────────────── this.labels = DEFAULT_LABELS; this.selectedLabel = null; this.deliveryMessage = null; this.hasError = false; this.errorMessage = ''; this.isLoadingPromises = false; // ── Login-cycle state (bullet delivery integration) ─────────────────── this.selectedCityLoginCycle = null; this.selectedBranchLoginCycle = null; // ── Standard flow state (city list) ─────────────────────────────────── this.cities = []; this.selectedCity = null; this.isLoadingCities = false; // ── City-change modal state (non–login-cycle) ───────────────────────── this.cityPendingSelection = null; this.modalCities = []; this.isSearchingCities = false; this.citySearchCounter = 0; // ═══════════════════════════════════════════════════════════════════════ // Login-cycle (bullet delivery) integration // ═══════════════════════════════════════════════════════════════════════ this.onBulletDeliveryConfirmed = () => { this.applyBulletIntent(); }; this.handleCitySearch = (keyword) => { clearTimeout(this.citySearchTimer); if (!keyword.trim()) { this.modalCities = [...this.cities]; this.isSearchingCities = false; return; } this.citySearchTimer = setTimeout(() => this.searchCitiesRemote(keyword), 300); }; // ═══════════════════════════════════════════════════════════════════════ // Header click (dispatches to correct flow) // ═══════════════════════════════════════════════════════════════════════ this.handleHeaderClick = (e) => { e.stopPropagation(); if (this.isLoginCycleEnabled) { this.openBulletDeliveryModal(); } else { void this.openCityChangeModal(); } }; } // ═══════════════════════════════════════════════════════════════════════ // Lifecycle // ═══════════════════════════════════════════════════════════════════════ async componentDidLoad() { await salla.onReady(); await salla.lang.onLoaded(); this.labels = loadLabels(); this.isDeliveryPromiseEnabled = salla.config.get('store.features', []).includes('delivery-promises'); this.isLoginCycleEnabled = salla.config.get('store.features', []).includes('bullet-delivery-v2'); this.productId = salla.config.get('page.id'); if (!this.isDeliveryPromiseEnabled) { this.canRender = false; return; } if (this.isLoginCycleEnabled) { await this.initLoginCycleFlow(); } else { this.selectedLabel = `${this.labels.deliveryTo} ${this.labels.selectCity}`; await this.loadCities(); } } disconnectedCallback() { window.removeEventListener('bulletDeliveryConfirmed', this.onBulletDeliveryConfirmed); } getIPDeliveryLocation() { const configPath = "store.shipping.delivery_location"; const ipAddress = { cityId: salla.config.get(`${configPath}.city_id`), cityName: salla.config.get(`${configPath}.city_name`), }; return localStorage.getItem(OVERRIDE_IP_KEY) ? JSON.parse(localStorage.getItem(OVERRIDE_IP_KEY)) : ipAddress; } async initLoginCycleFlow() { const hasIntent = this.applyBulletIntent(); const { cityId, cityName } = this.getIPDeliveryLocation(); log("delivery promise login cycle flow getIPDeliveryLocation", this.getIPDeliveryLocation()); if (cityId && cityName && !hasIntent) { this.selectedLabel = `${this.labels.deliveryTo} ${cityName}`; await this.loadDeliveryMessage(cityId, 'city'); } this.canRender = true; window.addEventListener('bulletDeliveryConfirmed', this.onBulletDeliveryConfirmed); } applyBulletIntent() { const resolved = resolveIntent(); if (!resolved) { this.selectedLabel = `${this.labels.deliveryTo} ${this.labels.selectCity}`; this.selectedCityLoginCycle = null; this.selectedBranchLoginCycle = null; this.deliveryMessage = null; return false; } if (resolved.type === 'address') { this.selectedCityLoginCycle = resolved.option; this.selectedBranchLoginCycle = null; this.selectedLabel = `${this.labels.deliveryTo} ${resolved.option.name || this.labels.selectCity}`; void this.loadDeliveryMessage(resolved.option.id, 'city'); } else { this.selectedBranchLoginCycle = resolved.option; this.selectedCityLoginCycle = null; this.selectedLabel = `${this.labels.pickupFromBranch} ${resolved.option.name}`; void this.loadDeliveryMessage(resolved.option.id, 'branch'); } return true; } openBulletDeliveryModal() { salla.event.emit('salla::bullet-delivery.modal.open.requested', buildBulletOpenPayload()); } // ═══════════════════════════════════════════════════════════════════════ // Standard flow (city list, no login cycle) // ═══════════════════════════════════════════════════════════════════════ async loadCities() { this.isLoadingCities = true; this.hasError = false; const { cityId, cityName } = this.getIPDeliveryLocation(); log("delivery promise standard flow getIPDeliveryLocation", this.getIPDeliveryLocation()); try { const { cities } = await fetchCities(this.productId); if (!cities.length) { this.canRender = false; return; } this.cities = cities; this.canRender = true; // get delivery message for the IP location if (cityId && cityName) { this.selectedLabel = `${this.labels.deliveryTo} ${cityName}`; const autoCity = this.cities.find(c => String(c.id) === String(cityId)); if (autoCity) { log("delivery promise standard flow autoCity found", autoCity); this.selectedCity = autoCity; } await this.loadDeliveryMessage(cityId, 'city'); } } catch (error) { this.hasError = true; this.errorMessage = error.message || 'Failed to load cities'; this.canRender = false; } finally { this.isLoadingCities = false; } } // ═══════════════════════════════════════════════════════════════════════ // Delivery message (shared by both flows) // ═══════════════════════════════════════════════════════════════════════ async loadDeliveryMessage(id, optionType) { this.isLoadingPromises = true; this.hasError = false; this.deliveryMessage = null; try { this.deliveryMessage = await fetchDeliveryMessage(this.productId, id, optionType, this.isLoginCycleEnabled); } catch (error) { this.hasError = true; this.errorMessage = optionType === 'city' ? this.labels.errorDeliveryPromise : this.labels.errorPickupPromise; } finally { this.isLoadingPromises = false; } } // ═══════════════════════════════════════════════════════════════════════ // City-change modal handlers (non–login-cycle) // ═══════════════════════════════════════════════════════════════════════ async openCityChangeModal() { if (!this.cityModalRef) return; this.cityPendingSelection = null; this.modalCities = [...this.cities]; await this.cityModalRef.setTitle(this.labels.changeCityTitle); await this.cityModalRef.open(); } async searchCitiesRemote(keyword) { const requestId = ++this.citySearchCounter; this.isSearchingCities = true; try { const { cities } = await fetchCities(this.productId, keyword); if (requestId !== this.citySearchCounter) return; this.modalCities = cities; } catch { if (requestId !== this.citySearchCounter) return; this.modalCities = []; } finally { if (requestId === this.citySearchCounter) { this.isSearchingCities = false; } } } async handleConfirmCityModal() { if (!this.cityPendingSelection) { await this.cityModalRef?.close(); return; } const city = this.cityPendingSelection; this.selectedCity = city; this.selectedLabel = `${this.labels.deliveryTo} ${getCityDisplayName(city)}`; await this.loadDeliveryMessage(city.id, 'city'); await this.cityModalRef?.close(); } // ═══════════════════════════════════════════════════════════════════════ // Render helpers // ═══════════════════════════════════════════════════════════════════════ renderLoadingSkeleton() { return (h(Host, { class: "s-delivery-promise-wrapper s-delivery-promise-skeleton" }, h("div", { class: "s-delivery-promise-container" }, h("div", { class: "s-delivery-promise-header s-delivery-promise-header-skeleton" }, h("div", { class: "s-delivery-promise-location" }, h("salla-skeleton", { height: "14px", width: "180px" })), h("salla-skeleton", { height: "18px", width: "18px" })), h("div", { class: "s-delivery-promise-loading" }, h("salla-skeleton", { height: "20px", width: "80%" }))))); } renderDeliveryMessage() { if (this.isLoadingPromises) { return (h("div", { class: "s-delivery-promise-loading" }, h("salla-skeleton", { height: "20px", width: "80%" }))); } if (!this.deliveryMessage) return null; return h("div", { class: "s-delivery-promise-message" }, this.deliveryMessage); } renderCityModalBody() { return (h("div", { class: "s-delivery-promise-modal-body" }, h("salla-searchable-dropdown", { label: this.labels.cityFieldLabel, placeholder: this.labels.modalSearchPlaceholder, items: this.modalCities, selectedItem: this.cityPendingSelection, searching: this.isSearchingCities, required: true, noResultsText: this.labels.noResults, inputId: "s-delivery-promise-modal-search", onItemSelected: (e) => { this.cityPendingSelection = e.detail; }, onSearchInput: (e) => { this.handleCitySearch(e.detail); } }))); } renderCityChangeModal() { if (this.isLoginCycleEnabled) return null; return (h("salla-modal", { id: "s-delivery-promise-city-modal", ref: el => { this.cityModalRef = el; }, class: "s-delivery-promise-city-modal", isClosable: true, width: "md", "modal-title": this.labels.changeCityTitle, subTitle: this.labels.changeCitySubtitle }, this.renderCityModalBody(), h("salla-button", { slot: "footer", width: "wide", disabled: !this.cityPendingSelection, onClick: () => this.handleConfirmCityModal() }, !this.isLoadingPromises && this.labels.confirmAddress))); } // ═══════════════════════════════════════════════════════════════════════ // Main render // ═══════════════════════════════════════════════════════════════════════ render() { if (!this.isDeliveryPromiseEnabled) return null; if (this.isLoadingCities) return this.renderLoadingSkeleton(); if (!this.canRender) return null; return (h(Host, { class: "s-delivery-promise-wrapper" }, h("div", { class: "s-delivery-promise-container" }, h("div", { class: "s-delivery-promise-header", onClick: this.handleHeaderClick }, h("div", { class: "s-delivery-promise-location" }, h("span", { class: "s-delivery-promise-title" }, this.selectedLabel)), h("i", { class: `sicon-keyboard_arrow_down s-delivery-promise-arrow` })), this.hasError && h("div", { class: "s-delivery-promise-error" }, this.errorMessage), !this.hasError && this.renderDeliveryMessage()), this.renderCityChangeModal())); } static get is() { return "salla-delivery-promise"; } static get states() { return { "isDeliveryPromiseEnabled": {}, "isLoginCycleEnabled": {}, "canRender": {}, "labels": {}, "selectedLabel": {}, "deliveryMessage": {}, "hasError": {}, "errorMessage": {}, "isLoadingPromises": {}, "selectedCityLoginCycle": {}, "selectedBranchLoginCycle": {}, "cities": {}, "selectedCity": {}, "isLoadingCities": {}, "cityPendingSelection": {}, "modalCities": {}, "isSearchingCities": {} }; } static get elementRef() { return "host"; } }