@salla.sa/twilight-components
Version:
Salla Web Component
398 lines (392 loc) • 21.8 kB
JavaScript
/*!
* Crafted with ❤ by Salla
*/
import { r as registerInstance, h, H as Host, a as getElement } from './index-BHYtfMwX.js';
const DEBUG_KEY = 'salla-delivery-promise-debug';
function log(message, data) {
if (localStorage.getItem(DEBUG_KEY)) {
data !== undefined ? console.log(message, data) : console.log(message);
}
}
/** Fetch available cities for a product's delivery promises. */
async function fetchCities(productId, keyword) {
log('fetchCities start', { productId, keyword });
const queryString = keyword != null ? `?keyword=${encodeURIComponent(keyword)}` : '';
const response = await salla.api.request(salla.url.api(`products/${productId}/delivery-promises/cities${queryString}`));
if (!response.success || !Array.isArray(response.data) || response.data.length === 0) {
log('fetchCities: no data', { response });
return { cities: [] };
}
log('fetchCities success', { count: response.data.length });
return { cities: response.data };
}
/** Fetch the delivery promise message for a given city or branch. */
async function fetchDeliveryMessage(productId, id, optionType, isLoginCycleEnabled) {
log('fetchDeliveryMessage start', { productId, id, optionType });
let queryString = '';
if (optionType === 'city' && !isLoginCycleEnabled) {
queryString = `?reference_id=${id}`;
}
else {
queryString = `?reference_id=${id}&reference_type=${optionType}`;
}
const response = (await salla.api.withoutNotifier(() => salla.api.request(salla.url.api(`products/${productId}/delivery-promises${queryString}`))));
if (!response.success) {
log('fetchDeliveryMessage error', { response });
throw new Error(response.error?.message || 'Failed to fetch delivery promise');
}
const message = response.data?.message?.trim();
log('fetchDeliveryMessage success', { message });
return message || null;
}
const INTENT_KEY = 'bullet_delivery_intent';
function getIntentStorage() {
const rememberLastSession = Boolean(salla.config.get('store.settings.bullet_delivery.settings.remember_last_session'));
return rememberLastSession ? salla.storage.store : salla.storage.session;
}
function getStoredIntent() {
const raw = getIntentStorage().get(INTENT_KEY);
return raw && typeof raw === 'object' ? raw : null;
}
/** Resolve the stored intent into a city or branch selection. Returns null when no usable intent. */
function resolveIntent() {
const intent = getStoredIntent();
if (!intent)
return null;
if (intent.type === 'address') {
const city = intent.address_details?.city;
const cityId = city?.id != null ? Number(city.id) : intent.city_id != null ? Number(intent.city_id) : null;
const cityName = city?.name?.trim() || '';
if (cityId != null) {
return { type: 'address', option: { id: cityId, name: cityName } };
}
return null;
}
if (intent.type === 'branch') {
const branchId = intent.branch_id != null
? Number(intent.branch_id)
: intent.branch_details?.id != null
? Number(intent.branch_details.id)
: null;
const branchName = intent.branch_details?.name?.trim() || '';
if (branchId != null) {
return { type: 'branch', option: { id: branchId, name: branchName } };
}
return null;
}
return null;
}
/** Build the payload for opening the bullet delivery modal with preselection. */
function buildBulletOpenPayload() {
const intent = getStoredIntent();
const payload = {};
if (intent?.type === 'address' && intent.address_id != null) {
payload.preselected_address_id = Number(intent.address_id);
}
if (intent?.type === 'branch') {
const branchId = intent.branch_id ?? intent.branch_details?.id;
if (branchId != null) {
payload.preselected_branch_id = Number(branchId);
}
}
return payload;
}
const DEFAULT_LABELS = {
deliveryTo: 'توصيل إلى',
pickupFromBranch: 'الاستلام من فرع',
noResults: 'لا توجد نتائج',
selectCity: 'اختر المدينة',
changeCityTitle: 'تغيير المدينة',
changeCitySubtitle: '',
cityFieldLabel: 'المدينة',
modalSearchPlaceholder: 'ابحث عن مدينة',
confirmAddress: 'تأكيد العنوان',
errorDeliveryPromise: 'لا يتوفر وعد تسليم لهذه المدينة',
errorPickupPromise: 'لا يتوفر وعد تسليم لهذا الفرع',
};
function loadLabels() {
return {
deliveryTo: salla.lang.getWithDefault('pages.products.promise_deliver_to', DEFAULT_LABELS.deliveryTo),
pickupFromBranch: salla.lang.getWithDefault('pages.products.promise_pickup_from_branch', DEFAULT_LABELS.pickupFromBranch),
noResults: salla.lang.getWithDefault('common.elements.no_options', DEFAULT_LABELS.noResults),
selectCity: salla.lang.getWithDefault('common.elements.select_city', DEFAULT_LABELS.selectCity),
changeCityTitle: salla.lang.getWithDefault('pages.products.promise_change_city_title', DEFAULT_LABELS.changeCityTitle),
changeCitySubtitle: salla.lang.getWithDefault('pages.products.promise_change_city_subtitle', 'قد تتغيّر مدة التوصيل حسب المدينة.'),
cityFieldLabel: salla.lang.getWithDefault('pages.products.promise_city_field', DEFAULT_LABELS.cityFieldLabel),
modalSearchPlaceholder: salla.lang.getWithDefault('pages.products.promise_search_city', DEFAULT_LABELS.modalSearchPlaceholder),
confirmAddress: salla.lang.getWithDefault('pages.checkout.confirm_address', DEFAULT_LABELS.confirmAddress),
errorDeliveryPromise: salla.lang.getWithDefault('pages.products.promise_delivery_not_available', DEFAULT_LABELS.errorDeliveryPromise),
errorPickupPromise: salla.lang.getWithDefault('pages.products.promise_pickup_not_available', DEFAULT_LABELS.errorPickupPromise),
};
}
/** Localized city display name (English when available and user language is not Arabic). */
function getCityDisplayName(city) {
const lang = salla.config.get('user.language_code');
if (lang && lang !== 'ar' && city.name_en?.trim()) {
return city.name_en.trim();
}
return city.name;
}
const OVERRIDE_IP_KEY = 'salla-bullet-delivery-override-ip';
const SallaDeliveryPromise = class {
constructor(hostRef) {
registerInstance(this, hostRef);
// ── 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()));
}
get host() { return getElement(this); }
};
export { SallaDeliveryPromise as salla_delivery_promise };