@salla.sa/twilight-components
Version:
Salla Web Component
297 lines (296 loc) • 16.6 kB
JavaScript
/*!
* 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"; }
}