fix: plan location limit counts switched-on locations; widget empty state

- storefront applied maxLocations to the first locations by priority, even switched-off
  ones, so a Starter shop whose enabled location wasn't first got no methods
- switching on a location beyond the plan limit now explains why instead of being ignored
- widget shows a message (en/fr) when no method is available and clears aria-busy

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
metatroncubeswdev 2026-10-15 05:26:03 -04:00
parent 38e82f791e
commit aacd9a8e06
7 changed files with 17 additions and 6 deletions

View File

@ -60,6 +60,11 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
if (intent === "basics") { if (intent === "basics") {
const lat = formString(fd, "lat"); const lat = formString(fd, "lat");
const lng = formString(fd, "lng"); const lng = formString(fd, "lng");
if (formBool(fd, "enabled") && !row.enabled) {
const on = cfg.locations.filter((l) => l.enabled && l.id !== row.id).length;
if (on >= features.maxLocations)
return { ok: false, message: `Your plan takes bookings at ${features.maxLocations} location${features.maxLocations === 1 ? "" : "s"}. Switch another location off, or upgrade in Settings.` };
}
await save({ await save({
name: formString(fd, "name") || "Location", name: formString(fd, "name") || "Location",
address: formString(fd, "address"), address: formString(fd, "address"),

View File

@ -59,7 +59,8 @@ async function context(domain: string, admin: AdminGraphql | undefined, q: z.inf
const today = localParts(now, cfg.shop.timezone).date; const today = localParts(now, cfg.shop.timezone).date;
const usage = await reservations.usage(cfg.shop.id, addDays(today, -1), addDays(today, 370), new Date(now), q.token); const usage = await reservations.usage(cfg.shop.id, addDays(today, -1), addDays(today, 370), new Date(now), q.token);
const f = features(cfg.shop.plan); const f = features(cfg.shop.plan);
const locations = cfg.locations.slice(0, f.maxLocations); // The plan limit counts locations that are switched on (imported ones start off), in priority order.
const locations = cfg.locations.filter((l) => l.enabled).slice(0, f.maxLocations);
return { cfg, now, cart, inventory, feeLines, address, usage, locations, f }; return { cfg, now, cart, inventory, feeLines, address, usage, locations, f };
} }

File diff suppressed because one or more lines are too long

View File

@ -24,6 +24,7 @@
"prep": {{ 'datewise.prep' | t | json }}, "prep": {{ 'datewise.prep' | t | json }},
"free": {{ 'datewise.free' | t | json }}, "free": {{ 'datewise.free' | t | json }},
"fee": {{ 'datewise.fee' | t | json }}, "fee": {{ 'datewise.fee' | t | json }},
"none": {{ 'datewise.none' | t | json }},
"nextAvailable": {{ 'datewise.next_available' | t | json }}, "nextAvailable": {{ 'datewise.next_available' | t | json }},
"offer": {{ 'datewise.offer' | t | json }}, "offer": {{ 'datewise.offer' | t | json }},
"error": {{ 'datewise.error' | t | json }} "error": {{ 'datewise.error' | t | json }}

View File

@ -18,6 +18,7 @@
"fee": "fee", "fee": "fee",
"next_available": "Next available", "next_available": "Next available",
"offer": "A place opened up and we're holding it for you", "offer": "A place opened up and we're holding it for you",
"error": "Something went wrong. Please try again." "error": "Something went wrong. Please try again.",
"none": "No delivery or pickup times are available for this cart right now."
} }
} }

View File

@ -18,6 +18,7 @@
"fee": "frais", "fee": "frais",
"next_available": "Prochaine disponibilité", "next_available": "Prochaine disponibilité",
"offer": "Une place s'est libérée et nous la gardons pour vous", "offer": "Une place s'est libérée et nous la gardons pour vous",
"error": "Une erreur s'est produite. Veuillez réessayer." "error": "Une erreur s'est produite. Veuillez réessayer.",
"none": "Aucun créneau de livraison ou de retrait n'est disponible pour ce panier pour le moment."
} }
} }

View File

@ -21,7 +21,7 @@
held: "Held for you for", expired: "Your hold expired. Tap the time again to check it's still free.", left: "left", full: "Full", held: "Held for you for", expired: "Your hold expired. Tap the time again to check it's still free.", left: "left", full: "Full",
postal: "Postal / ZIP code", check: "Check", waitlist: "Join the waitlist", joined: "You're on the waitlist. We'll message you if a place opens.", postal: "Postal / ZIP code", check: "Check", waitlist: "Join the waitlist", joined: "You're on the waitlist. We'll message you if a place opens.",
dispatch: "Ships this day (not an arrival date)", change: "Change", prep: "Earliest times include preparation for", free: "Free", fee: "fee", dispatch: "Ships this day (not an arrival date)", change: "Change", prep: "Earliest times include preparation for", free: "Free", fee: "fee",
nextAvailable: "Next available", offer: "A place opened up and we're holding it for you", error: "Something went wrong. Please try again." none: "No delivery or pickup times are available for this cart right now.", nextAvailable: "Next available", offer: "A place opened up and we're holding it for you", error: "Something went wrong. Please try again."
}, CFG.text || {}); }, CFG.text || {});
var ROOT = ((window.Shopify && Shopify.routes && Shopify.routes.root) || "/").replace(/\/?$/, "/"); var ROOT = ((window.Shopify && Shopify.routes && Shopify.routes.root) || "/").replace(/\/?$/, "/");
var API = ROOT + "apps/datewise"; var API = ROOT + "apps/datewise";
@ -203,7 +203,9 @@
var h = ""; var h = "";
if (heading) h += '<h2 class="dw-h">' + esc(heading) + "</h2>"; if (heading) h += '<h2 class="dw-h">' + esc(heading) + "</h2>";
h += '<div class="dw-live" role="status" aria-live="polite">' + (S.message ? '<p class="dw-msg dw-' + S.messageTone + '">' + esc(S.message) + "</p>" : "") + "</div>"; h += '<div class="dw-live" role="status" aria-live="polite">' + (S.message ? '<p class="dw-msg dw-' + S.messageTone + '">' + esc(S.message) + "</p>" : "") + "</div>";
if (!d) { root.innerHTML = h + (S.loading ? '<div class="dw-skeleton" aria-hidden="true"></div>' : ""); root.setAttribute("aria-busy", S.loading); return; } root.setAttribute("aria-busy", S.loading);
if (!d) { root.innerHTML = h + (S.loading ? '<div class="dw-skeleton" aria-hidden="true"></div>' : ""); return; }
if (!d.methods.length) { root.innerHTML = h + '<p class="dw-note">' + esc(T.none) + "</p>"; return; }
// methods // methods
h += '<div class="dw-methods" role="group" aria-label="Delivery method">'; h += '<div class="dw-methods" role="group" aria-label="Delivery method">';