diff --git a/extensions/datetime-widget/assets/datetime-widget.css b/extensions/datetime-widget/assets/datetime-widget.css
index deb23e6..e690526 100644
--- a/extensions/datetime-widget/assets/datetime-widget.css
+++ b/extensions/datetime-widget/assets/datetime-widget.css
@@ -6,6 +6,16 @@
min-height: 3.5rem; /* reserves space before JS renders content, to avoid layout shift */
}
+/* Cart-page auto-injection (datetime-widget.ts's maybeAutoPlaceOnCart) lands
+ the widget as a sibling immediately before the theme's own Checkout
+ button, which can be inside a narrow flex/inline-sized wrapper — force
+ full width and give it breathing room so it doesn't get visually
+ squeezed regardless of that wrapper's own layout rules. */
+.dd-widget--cart-injected {
+ width: 100%;
+ margin-block: 1rem;
+}
+
.dd-widget__heading {
font-size: 1rem;
font-weight: 600;
diff --git a/extensions/datetime-widget/assets/datetime-widget.js b/extensions/datetime-widget/assets/datetime-widget.js
index 4371b46..ecc3c90 100644
--- a/extensions/datetime-widget/assets/datetime-widget.js
+++ b/extensions/datetime-widget/assets/datetime-widget.js
@@ -1 +1 @@
-"use strict";(()=>{var _=Object.defineProperty;var S=(i,t,e)=>t in i?_(i,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):i[t]=e;var l=(i,t,e)=>S(i,typeof t!="symbol"?t+"":t,e);var M="/apps/scheduling";function y(i){let t=Math.floor(i/60),e=i%60,n=t<12?"AM":"PM";return`${t%12===0?12:t%12}:${e.toString().padStart(2,"0")} ${n}`}function p(i){return h(i.slice(0,10))}function f(i){return i.arrivalRangeStart&&i.arrivalRangeEnd?`Arrives ${p(i.arrivalRangeStart)}\u2013${p(i.arrivalRangeEnd)}`:i.arrivalRangeStart?`Arrives from ${p(i.arrivalRangeStart)}`:i.arrivalRangeEnd?`Arrives by ${p(i.arrivalRangeEnd)}`:`${y(i.startMin)}\u2013${y(i.endMin)}`}function h(i){let[t,e,n]=i.split("-").map(Number);return new Date(Date.UTC(t,e-1,n)).toLocaleDateString(void 0,{weekday:"short",month:"short",day:"numeric",timeZone:"UTC"})}function E(i){let t=i.dataset,e=[];return t.showShipping==="true"&&e.push({value:"SHIPPING",label:t.labelShipping||"Shipping",attrLabel:t.attrLabelShipping||"Shipping date"}),t.showLocalDelivery==="true"&&e.push({value:"LOCAL_DELIVERY",label:t.labelLocalDelivery||"Local delivery",attrLabel:t.attrLabelLocalDelivery||"Delivery date"}),t.showPickup==="true"&&e.push({value:"PICKUP",label:t.labelPickup||"Pickup",attrLabel:t.attrLabelPickup||"Pickup date"}),{root:i,heading:t.heading||"",locationId:t.locationId||null,googleMapsApiKey:t.googleMapsApiKey||null,methods:e,labels:{chooseDate:t.labelChooseDate||"Choose a date",chooseTime:t.labelChooseTime||"Choose a time",noDates:t.labelNoDates||"No dates are available right now.",confirmed:t.labelConfirmed||"Confirmed for",change:t.labelChange||"Change",loading:t.labelLoading||"Loading available dates\u2026",error:t.labelError||"Couldn't load available dates. Please try again.",postalCodeLabel:t.labelPostalCode||"Enter your postal/ZIP code",postalCodeSubmit:t.labelPostalCodeSubmit||"Check availability",outOfArea:t.labelOutOfArea||"Sorry, we don't deliver to this address."}}}async function C(){var e;let t=await(await fetch("/cart.js",{headers:{Accept:"application/json"}})).json();return{token:t.token,lines:((e=t.items)!=null?e:[]).map(n=>{var a,o;return{vendor:(a=n.vendor)!=null?a:"",productType:(o=n.product_type)!=null?o:"",productId:`gid://shopify/Product/${n.product_id}`}})}}async function I(i,t,e,n){let a=new URLSearchParams({method:i,days:"14"});t&&a.set("locationId",t),n&&a.set("postalCode",n),e.lines.length>0&&(a.set("cartLines",JSON.stringify(e.lines.map(r=>({vendor:r.vendor,productType:r.productType})))),a.set("productIds",e.lines.map(r=>r.productId).join(",")));let o=await fetch(`${M}/availability?${a.toString()}`,{headers:{Accept:"application/json"}}),s=await o.json();if(!o.ok)throw new Error(s.error||`Request failed (${o.status})`);return s}async function R(i){let t=await fetch(`${M}/hold`,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(i)}),e=await t.json();return!t.ok&&i.intent==="create"?{success:!1,error:e.error||"Slot unavailable"}:e}async function k(i,t,e){await fetch("/cart/update.js",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({attributes:{[i]:e,...t}})})}var m=null;function A(i){return m||(m=new Promise((t,e)=>{let n="__ddMapsReady";window[n]=()=>t();let a=document.createElement("script");a.src=`https://maps.googleapis.com/maps/api/js?key=${encodeURIComponent(i)}&callback=${n}`,a.async=!0,a.onerror=()=>e(new Error("Failed to load Google Maps")),document.head.appendChild(a)}),m)}async function P(i,t,e,n,a){try{await A(t);let o=window.google,s=new o.maps.Map(i,{center:{lat:e,lng:n},zoom:13});new o.maps.Marker({position:{lat:e,lng:n},map:s,title:a})}catch{i.hidden=!0}}var b=class{constructor(t){l(this,"config");l(this,"el",{heading:document.createElement("h3"),methodRow:document.createElement("div"),postalRow:document.createElement("div"),mapContainer:document.createElement("div"),dateRow:document.createElement("div"),timeRow:document.createElement("div"),status:document.createElement("p"),confirmation:document.createElement("div")});l(this,"selectedMethod",null);l(this,"selectedDate",null);l(this,"availability",null);l(this,"heldSlot",null);this.config=t}mount(){let{root:t,heading:e,methods:n}=this.config;t.classList.add("dd-widget--ready"),t.innerHTML="",n.length!==0&&(e&&(this.el.heading.className="dd-widget__heading",this.el.heading.textContent=e,t.appendChild(this.el.heading)),this.el.methodRow.className="dd-widget__row dd-widget__methods",this.el.postalRow.className="dd-widget__row dd-widget__postal",this.el.postalRow.hidden=!0,this.el.mapContainer.className="dd-widget__map",this.el.mapContainer.hidden=!0,this.el.dateRow.className="dd-widget__row dd-widget__dates",this.el.timeRow.className="dd-widget__row dd-widget__times",this.el.status.className="dd-widget__status",this.el.confirmation.className="dd-widget__confirmation",this.el.confirmation.hidden=!0,t.append(this.el.confirmation,this.el.methodRow,this.el.postalRow,this.el.mapContainer,this.el.dateRow,this.el.timeRow,this.el.status),n.length===1?this.selectMethod(n[0]):this.renderMethods())}renderMethods(){var t;this.el.methodRow.innerHTML="";for(let e of this.config.methods){let n=document.createElement("button");n.type="button",n.className="dd-widget__pill",n.textContent=e.label,n.setAttribute("aria-pressed",String(((t=this.selectedMethod)==null?void 0:t.value)===e.value)),n.addEventListener("click",()=>this.selectMethod(e)),this.el.methodRow.appendChild(n)}}async selectMethod(t){if(this.selectedMethod=t,this.selectedDate=null,this.el.timeRow.innerHTML="",this.el.dateRow.innerHTML="",this.el.mapContainer.hidden=!0,this.el.confirmation.hidden=!1,this.el.confirmation.hidden=!0,this.config.methods.length>1&&this.renderMethods(),t.value==="LOCAL_DELIVERY"){this.renderPostalCodeInput(t);return}this.el.postalRow.hidden=!0,await this.loadAvailability(t)}renderPostalCodeInput(t){this.el.postalRow.hidden=!1,this.el.postalRow.innerHTML="",this.el.status.textContent="";let e=document.createElement("input");e.type="text",e.className="dd-widget__input",e.placeholder=this.config.labels.postalCodeLabel,e.setAttribute("aria-label",this.config.labels.postalCodeLabel);let n=document.createElement("button");n.type="button",n.className="dd-widget__pill",n.textContent=this.config.labels.postalCodeSubmit,n.addEventListener("click",async()=>{let a=e.value.trim();a&&await this.loadAvailability(t,a)}),e.addEventListener("keydown",a=>{a.key==="Enter"&&n.click()}),this.el.postalRow.append(e,n)}async loadAvailability(t,e){this.el.status.textContent=this.config.labels.loading,this.el.dateRow.innerHTML="";try{let n=await C();if(this.availability=await I(t.value,this.config.locationId,n,e),!this.availability.locationId){this.el.status.textContent=this.availability.error||this.config.labels.outOfArea;return}t.value==="PICKUP"&&this.config.googleMapsApiKey&&this.showPickupMap(),this.renderDates()}catch{this.el.status.textContent=this.config.labels.error}}showPickupMap(){var e;let t=this.availability;!(t!=null&&t.locationLat)||!(t!=null&&t.locationLng)||!this.config.googleMapsApiKey||(this.el.mapContainer.hidden=!1,P(this.el.mapContainer,this.config.googleMapsApiKey,t.locationLat,t.locationLng,(e=t.locationName)!=null?e:""))}renderDates(){var e,n;let t=Object.keys((n=(e=this.availability)==null?void 0:e.dates)!=null?n:{}).sort();if(this.el.dateRow.innerHTML="",t.length===0){this.el.status.textContent=this.config.labels.noDates;return}this.el.status.textContent=this.config.labels.chooseDate;for(let a of t){let o=document.createElement("button");o.type="button",o.className="dd-widget__pill",o.textContent=h(a),o.setAttribute("aria-pressed",String(this.selectedDate===a)),o.addEventListener("click",()=>this.selectDate(a)),this.el.dateRow.appendChild(o)}}selectDate(t){var n,a;this.selectedDate=t;for(let o of Array.from(this.el.dateRow.children))o.setAttribute("aria-pressed",String(o.textContent===h(t)));let e=(a=(n=this.availability)==null?void 0:n.dates[t])!=null?a:[];this.el.timeRow.innerHTML="",this.el.status.textContent=this.config.labels.chooseTime;for(let o of e){let s=document.createElement("button");s.type="button",s.className="dd-widget__pill",s.textContent=f(o),s.addEventListener("click",()=>this.selectSlot(t,o)),this.el.timeRow.appendChild(s)}}async selectSlot(t,e){var r;let n=this.selectedMethod,a=this.availability,o=a.rate?` (${a.rate.label})`:"",s=e.arrivalRangeStart||e.arrivalRangeEnd?`Ships ${h(t)}, ${f(e)}${o}`:`${h(t)}, ${f(e)}${o}`;this.el.status.textContent=this.config.labels.loading;try{let u=await C(),v=await R({intent:"create",locationId:a.locationId,method:n.value,date:t,startMin:e.startMin,cartToken:u.token,cartLines:u.lines.map(g=>({vendor:g.vendor,productType:g.productType})),productIds:u.lines.map(g=>g.productId)});if(!v.success){this.el.status.textContent=v.error||this.config.labels.error,await this.selectMethod(n);return}this.heldSlot={locationId:a.locationId,method:n.value,date:t,startMin:e.startMin,cartToken:u.token,zoneId:(r=a.zoneId)!=null?r:null};let d={dd_method:n.value,dd_date:t,dd_start_min:String(e.startMin),dd_end_min:String(e.endMin),dd_location_id:a.locationId};a.zoneId&&(d.dd_zone_id=a.zoneId),a.rate&&(d.dd_rate_label=a.rate.label),e.arrivalRangeStart&&(d.dd_arrival_range_start=e.arrivalRangeStart),e.arrivalRangeEnd&&(d.dd_arrival_range_end=e.arrivalRangeEnd),await k(n.attrLabel,d,s),this.el.status.textContent="",this.el.methodRow.hidden=!0,this.el.postalRow.hidden=!0,this.el.mapContainer.hidden=!0,this.el.dateRow.hidden=!0,this.el.timeRow.hidden=!0,this.el.confirmation.hidden=!1,this.el.confirmation.innerHTML="";let w=document.createElement("p");w.textContent=`${this.config.labels.confirmed} ${s}`;let c=document.createElement("button");c.type="button",c.className="dd-widget__link",c.textContent=this.config.labels.change,c.addEventListener("click",()=>{this.heldSlot&&(R({intent:"release",...this.heldSlot}),this.heldSlot=null),this.el.methodRow.hidden=!1,this.el.dateRow.hidden=!1,this.el.timeRow.hidden=!1,this.el.confirmation.hidden=!0}),this.el.confirmation.append(w,c)}catch{this.el.status.textContent=this.config.labels.error}}};function L(){document.querySelectorAll("[data-dd-widget]").forEach(t=>{new b(E(t)).mount()})}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",L):L();})();
+"use strict";(()=>{var E=Object.defineProperty;var S=(i,t,e)=>t in i?E(i,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):i[t]=e;var l=(i,t,e)=>S(i,typeof t!="symbol"?t+"":t,e);var M="/apps/scheduling";function y(i){let t=Math.floor(i/60),e=i%60,n=t<12?"AM":"PM";return`${t%12===0?12:t%12}:${e.toString().padStart(2,"0")} ${n}`}function m(i){return h(i.slice(0,10))}function b(i){return i.arrivalRangeStart&&i.arrivalRangeEnd?`Arrives ${m(i.arrivalRangeStart)}\u2013${m(i.arrivalRangeEnd)}`:i.arrivalRangeStart?`Arrives from ${m(i.arrivalRangeStart)}`:i.arrivalRangeEnd?`Arrives by ${m(i.arrivalRangeEnd)}`:`${y(i.startMin)}\u2013${y(i.endMin)}`}function h(i){let[t,e,n]=i.split("-").map(Number);return new Date(Date.UTC(t,e-1,n)).toLocaleDateString(void 0,{weekday:"short",month:"short",day:"numeric",timeZone:"UTC"})}function _(i){let t=i.dataset,e=[];return t.showShipping==="true"&&e.push({value:"SHIPPING",label:t.labelShipping||"Shipping",attrLabel:t.attrLabelShipping||"Shipping date"}),t.showLocalDelivery==="true"&&e.push({value:"LOCAL_DELIVERY",label:t.labelLocalDelivery||"Local delivery",attrLabel:t.attrLabelLocalDelivery||"Delivery date"}),t.showPickup==="true"&&e.push({value:"PICKUP",label:t.labelPickup||"Pickup",attrLabel:t.attrLabelPickup||"Pickup date"}),{root:i,heading:t.heading||"",locationId:t.locationId||null,googleMapsApiKey:t.googleMapsApiKey||null,methods:e,labels:{chooseDate:t.labelChooseDate||"Choose a date",chooseTime:t.labelChooseTime||"Choose a time",noDates:t.labelNoDates||"No dates are available right now.",confirmed:t.labelConfirmed||"Confirmed for",change:t.labelChange||"Change",loading:t.labelLoading||"Loading available dates\u2026",error:t.labelError||"Couldn't load available dates. Please try again.",postalCodeLabel:t.labelPostalCode||"Enter your postal/ZIP code",postalCodeSubmit:t.labelPostalCodeSubmit||"Check availability",outOfArea:t.labelOutOfArea||"Sorry, we don't deliver to this address."}}}async function C(){var e;let t=await(await fetch("/cart.js",{headers:{Accept:"application/json"}})).json();return{token:t.token,lines:((e=t.items)!=null?e:[]).map(n=>{var a,o;return{vendor:(a=n.vendor)!=null?a:"",productType:(o=n.product_type)!=null?o:"",productId:`gid://shopify/Product/${n.product_id}`}})}}async function k(i,t,e,n){let a=new URLSearchParams({method:i,days:"14"});t&&a.set("locationId",t),n&&a.set("postalCode",n),e.lines.length>0&&(a.set("cartLines",JSON.stringify(e.lines.map(s=>({vendor:s.vendor,productType:s.productType})))),a.set("productIds",e.lines.map(s=>s.productId).join(",")));let o=await fetch(`${M}/availability?${a.toString()}`,{headers:{Accept:"application/json"}}),r=await o.json();if(!o.ok)throw new Error(r.error||`Request failed (${o.status})`);return r}async function L(i){let t=await fetch(`${M}/hold`,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(i)}),e=await t.json();return!t.ok&&i.intent==="create"?{success:!1,error:e.error||"Slot unavailable"}:e}async function A(i,t,e){await fetch("/cart/update.js",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({attributes:{[i]:e,...t}})})}var p=null;function I(i){return p||(p=new Promise((t,e)=>{let n="__ddMapsReady";window[n]=()=>t();let a=document.createElement("script");a.src=`https://maps.googleapis.com/maps/api/js?key=${encodeURIComponent(i)}&callback=${n}`,a.async=!0,a.onerror=()=>e(new Error("Failed to load Google Maps")),document.head.appendChild(a)}),p)}async function T(i,t,e,n,a){try{await I(t);let o=window.google,r=new o.maps.Map(i,{center:{lat:e,lng:n},zoom:13});new o.maps.Marker({position:{lat:e,lng:n},map:r,title:a})}catch{i.hidden=!0}}var f=class{constructor(t){l(this,"config");l(this,"el",{heading:document.createElement("h3"),methodRow:document.createElement("div"),postalRow:document.createElement("div"),mapContainer:document.createElement("div"),dateRow:document.createElement("div"),timeRow:document.createElement("div"),status:document.createElement("p"),confirmation:document.createElement("div")});l(this,"selectedMethod",null);l(this,"selectedDate",null);l(this,"availability",null);l(this,"heldSlot",null);this.config=t}mount(){let{root:t,heading:e,methods:n}=this.config;t.classList.add("dd-widget--ready"),t.innerHTML="",n.length!==0&&(e&&(this.el.heading.className="dd-widget__heading",this.el.heading.textContent=e,t.appendChild(this.el.heading)),this.el.methodRow.className="dd-widget__row dd-widget__methods",this.el.postalRow.className="dd-widget__row dd-widget__postal",this.el.postalRow.hidden=!0,this.el.mapContainer.className="dd-widget__map",this.el.mapContainer.hidden=!0,this.el.dateRow.className="dd-widget__row dd-widget__dates",this.el.timeRow.className="dd-widget__row dd-widget__times",this.el.status.className="dd-widget__status",this.el.confirmation.className="dd-widget__confirmation",this.el.confirmation.hidden=!0,t.append(this.el.confirmation,this.el.methodRow,this.el.postalRow,this.el.mapContainer,this.el.dateRow,this.el.timeRow,this.el.status),n.length===1?this.selectMethod(n[0]):this.renderMethods())}renderMethods(){var t;this.el.methodRow.innerHTML="";for(let e of this.config.methods){let n=document.createElement("button");n.type="button",n.className="dd-widget__pill",n.textContent=e.label,n.setAttribute("aria-pressed",String(((t=this.selectedMethod)==null?void 0:t.value)===e.value)),n.addEventListener("click",()=>this.selectMethod(e)),this.el.methodRow.appendChild(n)}}async selectMethod(t){if(this.selectedMethod=t,this.selectedDate=null,this.el.timeRow.innerHTML="",this.el.dateRow.innerHTML="",this.el.mapContainer.hidden=!0,this.el.confirmation.hidden=!1,this.el.confirmation.hidden=!0,this.config.methods.length>1&&this.renderMethods(),t.value==="LOCAL_DELIVERY"){this.renderPostalCodeInput(t);return}this.el.postalRow.hidden=!0,await this.loadAvailability(t)}renderPostalCodeInput(t){this.el.postalRow.hidden=!1,this.el.postalRow.innerHTML="",this.el.status.textContent="";let e=document.createElement("input");e.type="text",e.className="dd-widget__input",e.placeholder=this.config.labels.postalCodeLabel,e.setAttribute("aria-label",this.config.labels.postalCodeLabel);let n=document.createElement("button");n.type="button",n.className="dd-widget__pill",n.textContent=this.config.labels.postalCodeSubmit,n.addEventListener("click",async()=>{let a=e.value.trim();a&&await this.loadAvailability(t,a)}),e.addEventListener("keydown",a=>{a.key==="Enter"&&n.click()}),this.el.postalRow.append(e,n)}async loadAvailability(t,e){this.el.status.textContent=this.config.labels.loading,this.el.dateRow.innerHTML="";try{let n=await C();if(this.availability=await k(t.value,this.config.locationId,n,e),!this.availability.locationId){this.el.status.textContent=this.availability.error||this.config.labels.outOfArea;return}t.value==="PICKUP"&&this.config.googleMapsApiKey&&this.showPickupMap(),this.renderDates()}catch{this.el.status.textContent=this.config.labels.error}}showPickupMap(){var e;let t=this.availability;!(t!=null&&t.locationLat)||!(t!=null&&t.locationLng)||!this.config.googleMapsApiKey||(this.el.mapContainer.hidden=!1,T(this.el.mapContainer,this.config.googleMapsApiKey,t.locationLat,t.locationLng,(e=t.locationName)!=null?e:""))}renderDates(){var e,n;let t=Object.keys((n=(e=this.availability)==null?void 0:e.dates)!=null?n:{}).sort();if(this.el.dateRow.innerHTML="",t.length===0){this.el.status.textContent=this.config.labels.noDates;return}this.el.status.textContent=this.config.labels.chooseDate;for(let a of t){let o=document.createElement("button");o.type="button",o.className="dd-widget__pill",o.textContent=h(a),o.setAttribute("aria-pressed",String(this.selectedDate===a)),o.addEventListener("click",()=>this.selectDate(a)),this.el.dateRow.appendChild(o)}}selectDate(t){var n,a;this.selectedDate=t;for(let o of Array.from(this.el.dateRow.children))o.setAttribute("aria-pressed",String(o.textContent===h(t)));let e=(a=(n=this.availability)==null?void 0:n.dates[t])!=null?a:[];this.el.timeRow.innerHTML="",this.el.status.textContent=this.config.labels.chooseTime;for(let o of e){let r=document.createElement("button");r.type="button",r.className="dd-widget__pill",r.textContent=b(o),r.addEventListener("click",()=>this.selectSlot(t,o)),this.el.timeRow.appendChild(r)}}async selectSlot(t,e){var s;let n=this.selectedMethod,a=this.availability,o=a.rate?` (${a.rate.label})`:"",r=e.arrivalRangeStart||e.arrivalRangeEnd?`Ships ${h(t)}, ${b(e)}${o}`:`${h(t)}, ${b(e)}${o}`;this.el.status.textContent=this.config.labels.loading;try{let u=await C(),w=await L({intent:"create",locationId:a.locationId,method:n.value,date:t,startMin:e.startMin,cartToken:u.token,cartLines:u.lines.map(g=>({vendor:g.vendor,productType:g.productType})),productIds:u.lines.map(g=>g.productId)});if(!w.success){this.el.status.textContent=w.error||this.config.labels.error,await this.selectMethod(n);return}this.heldSlot={locationId:a.locationId,method:n.value,date:t,startMin:e.startMin,cartToken:u.token,zoneId:(s=a.zoneId)!=null?s:null};let d={dd_method:n.value,dd_date:t,dd_start_min:String(e.startMin),dd_end_min:String(e.endMin),dd_location_id:a.locationId};a.zoneId&&(d.dd_zone_id=a.zoneId),a.rate&&(d.dd_rate_label=a.rate.label),e.arrivalRangeStart&&(d.dd_arrival_range_start=e.arrivalRangeStart),e.arrivalRangeEnd&&(d.dd_arrival_range_end=e.arrivalRangeEnd),await A(n.attrLabel,d,r),this.el.status.textContent="",this.el.methodRow.hidden=!0,this.el.postalRow.hidden=!0,this.el.mapContainer.hidden=!0,this.el.dateRow.hidden=!0,this.el.timeRow.hidden=!0,this.el.confirmation.hidden=!1,this.el.confirmation.innerHTML="";let v=document.createElement("p");v.textContent=`${this.config.labels.confirmed} ${r}`;let c=document.createElement("button");c.type="button",c.className="dd-widget__link",c.textContent=this.config.labels.change,c.addEventListener("click",()=>{this.heldSlot&&(L({intent:"release",...this.heldSlot}),this.heldSlot=null),this.el.methodRow.hidden=!1,this.el.dateRow.hidden=!1,this.el.timeRow.hidden=!1,this.el.confirmation.hidden=!0}),this.el.confirmation.append(v,c)}catch{this.el.status.textContent=this.config.labels.error}}},P=['form[action*="/cart"] button[name="checkout"]','form[action*="/cart"] input[name="checkout"]','[name="checkout"]',"#checkout",'a[href="/checkout"]'];function D(){for(let i of P){let t=document.querySelector(i);if(t)return t}return null}function N(){return window.location.pathname.replace(/\/+$/,"").endsWith("/cart")}function $(){if(!N()||document.querySelector("[data-dd-widget]"))return;let i=document.getElementById("dd-widget-cart-template");if(!i)return;let t=D();if(!t)return;let e=document.createElement("div");e.setAttribute("data-dd-widget",""),e.classList.add("dd-widget--cart-injected");for(let n of Array.from(i.attributes))n.name!=="id"&&e.setAttribute(n.name,n.value);t.insertAdjacentElement("beforebegin",e),new f(_(e)).mount()}function R(){document.querySelectorAll("[data-dd-widget]").forEach(t=>{new f(_(t)).mount()}),$()}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",R):R();})();
diff --git a/extensions/datetime-widget/blocks/app-embed.liquid b/extensions/datetime-widget/blocks/app-embed.liquid
index 4e08188..ff787fd 100644
--- a/extensions/datetime-widget/blocks/app-embed.liquid
+++ b/extensions/datetime-widget/blocks/app-embed.liquid
@@ -1,10 +1,99 @@
{{ 'datetime-widget.css' | asset_url | stylesheet_tag }}
+{% comment %}
+ Cross-theme cart placement (PRODUCT_STRATEGY.md competitive parity item):
+ a manually-placed app BLOCK only ever lands wherever the active theme's
+ own section schema declares an `@app` slot — many themes don't expose one
+ next to the cart's Checkout button at all, only "Add section" at the top
+ level, which is what was showing up as a disconnected block. Shopify
+ gives apps no supported way to inject a block into an arbitrary spot in a
+ theme's own section markup, so this app embed (always loaded site-wide,
+ independent of any block placement) instead emits an inert
+ carrying the widget's config as data-* attributes; datetime-widget.js
+ finds the theme's own Checkout button by CSS selector at runtime and
+ inserts a live widget immediately before it. Best-effort — it depends on
+ the theme's Checkout button being findable by one of a few common
+ selectors (see datetime-widget.ts's CHECKOUT_BUTTON_SELECTOR) — but it's
+ the only way to get placement that isn't at the mercy of whether a given
+ theme's cart section happens to support app blocks.
+{% endcomment %}
+{% if block.settings.auto_place_cart %}
+
+{% endif %}
+
{% schema %}
{
"name": "t:app_embed.name",
"target": "body",
- "settings": []
+ "settings": [
+ {
+ "type": "checkbox",
+ "id": "auto_place_cart",
+ "label": "t:app_embed.auto_place_cart_label",
+ "info": "t:app_embed.auto_place_cart_info",
+ "default": true
+ },
+ {
+ "type": "text",
+ "id": "heading",
+ "label": "t:app_embed.heading_label",
+ "default": "Choose your delivery date"
+ },
+ {
+ "type": "checkbox",
+ "id": "show_shipping",
+ "label": "t:app_embed.show_shipping_label",
+ "default": true
+ },
+ {
+ "type": "checkbox",
+ "id": "show_local_delivery",
+ "label": "t:app_embed.show_local_delivery_label",
+ "default": true
+ },
+ {
+ "type": "checkbox",
+ "id": "show_pickup",
+ "label": "t:app_embed.show_pickup_label",
+ "default": true
+ },
+ {
+ "type": "text",
+ "id": "location_id",
+ "label": "t:app_embed.location_id_label",
+ "info": "t:app_embed.location_id_info"
+ },
+ {
+ "type": "text",
+ "id": "google_maps_api_key",
+ "label": "t:app_embed.google_maps_api_key_label",
+ "info": "t:app_embed.google_maps_api_key_info"
+ }
+ ]
}
{% endschema %}
diff --git a/extensions/datetime-widget/locales/en.default.schema.json b/extensions/datetime-widget/locales/en.default.schema.json
index aa2c432..9548e5d 100644
--- a/extensions/datetime-widget/locales/en.default.schema.json
+++ b/extensions/datetime-widget/locales/en.default.schema.json
@@ -1,6 +1,16 @@
{
"app_embed": {
- "name": "Delivery Date & Time"
+ "name": "Delivery Date & Time",
+ "auto_place_cart_label": "Show automatically on the cart page",
+ "auto_place_cart_info": "Inserts the picker right before your theme's Checkout button, without needing to add a block in the theme editor. Turn this off if you've already placed the \"Date & time picker\" block manually and don't want it to show twice.",
+ "heading_label": "Heading",
+ "show_shipping_label": "Show Shipping",
+ "show_local_delivery_label": "Show Local Delivery",
+ "show_pickup_label": "Show Pickup",
+ "location_id_label": "Location ID (advanced)",
+ "location_id_info": "Leave blank to use the shop's default location.",
+ "google_maps_api_key_label": "Google Maps API key",
+ "google_maps_api_key_info": "Optional — shows a map for Pickup locations. Restrict this key to your store's domain in Google Cloud Console."
},
"datetime_picker": {
"name": "Date & Time Picker",
diff --git a/shopify.app.toml b/shopify.app.toml
index fb1b057..4bce65a 100644
--- a/shopify.app.toml
+++ b/shopify.app.toml
@@ -2,14 +2,14 @@
client_id = "fd503f515942a32aa8b811c1ab8c89d0"
name = "Metatron-delivery"
-# Placeholder — `shopify app dev` overwrites this on Shopify's live app
-# record automatically each session (via automatically_update_urls_on_dev
-# below), it does NOT rewrite this file. That auto-update only works
-# because shopify.web.toml exists at the repo root telling the CLI how to
-# run this project's Remix app; without it, the CLI has no local dev-server
-# port to build a tunnel URL from and silently keeps using whatever's here
-# — which is exactly what caused the embedded admin app to never load
-# during initial setup (see shopify.web.toml's own comment).
+# The app's real, permanent production URL — PM2 (ecosystem.config.cjs)
+# keeps the Remix server running here on the actual deployment server, per
+# .env's SHOPIFY_APP_URL/PORT on that machine. NOT a placeholder anymore:
+# automatically_update_urls_on_dev is deliberately OFF (see [build] below)
+# specifically so a local `shopify app dev` session never overwrites this
+# with a temporary Cloudflare tunnel URL and breaks the live app for real
+# usage. Only change this value (and redeploy) if the production domain
+# itself changes.
application_url = "https://metatron-delivery.thedomainnest.com"
embedded = true
@@ -105,16 +105,22 @@ api_version = "unstable"
uri = "/webhooks/events/placeholder"
# App proxy so the storefront Theme App Extension can call our backend
-# without CORS issues (see IMPLEMENTATION_PLAN.md §5.3). `shopify app dev`
-# points this at your dev tunnel automatically when
-# automatically_update_urls_on_dev is true (see [build] below); if it
-# doesn't, set it manually to `/apps/scheduling` — the
-# Remix routes it forwards to (apps.scheduling.*.tsx) assume that exact
-# prefix.
+# without CORS issues (see IMPLEMENTATION_PLAN.md §5.3). Points at the real
+# production domain — the Remix routes it forwards to (apps.scheduling.*.tsx)
+# assume the exact /apps/scheduling prefix below.
[app_proxy]
url = "https://metatron-delivery.thedomainnest.com/apps/scheduling"
subpath = "scheduling"
prefix = "apps"
[build]
-automatically_update_urls_on_dev = true
+# Deliberately OFF now that there's a real, permanent production URL above
+# — if this were true, running `shopify app dev` locally (e.g. to test a
+# theme/UI extension change) would silently overwrite application_url on
+# Shopify's live app record with a temporary Cloudflare tunnel, breaking
+# the deployed app for anyone using it until someone notices and redeploys
+# with the real URL. Local `shopify app dev` sessions still work fine for
+# testing extensions with this off; the embedded admin app just won't be
+# reachable through that session's tunnel (test it against the real
+# production domain above instead).
+automatically_update_urls_on_dev = false
diff --git a/widget-src/datetime-widget/datetime-widget.ts b/widget-src/datetime-widget/datetime-widget.ts
index 630036a..5362867 100644
--- a/widget-src/datetime-widget/datetime-widget.ts
+++ b/widget-src/datetime-widget/datetime-widget.ts
@@ -567,11 +567,71 @@ class DateTimeWidget {
}
}
+// Cross-theme cart placement: a manually-placed app BLOCK only ever lands
+// wherever the active theme's own section schema happens to declare an
+// `@app` slot — many themes only expose "Add section" for the cart's
+// checkout area, not "Add block" next to the actual Checkout button, which
+// is what shows up as a disconnected standalone section. There's no
+// Shopify-supported way for an app to inject a block into an arbitrary
+// spot in a theme's own markup, so instead: the app embed (blocks/
+// app-embed.liquid, loaded site-wide once merchants enable it, independent
+// of any block placement) emits an inert carrying the widget's config as data-* attributes. If we're
+// on the cart page, no block-placed widget already exists (avoids a
+// double render), and a Checkout button can be found by one of these
+// selectors, clone a live widget in immediately before it. Ordered
+// roughly most-to-least specific/reliable across common theme markup
+// patterns; the first match wins.
+const CHECKOUT_BUTTON_SELECTORS = [
+ 'form[action*="/cart"] button[name="checkout"]',
+ 'form[action*="/cart"] input[name="checkout"]',
+ '[name="checkout"]',
+ '#checkout',
+ 'a[href="/checkout"]',
+];
+
+function findCheckoutButton(): HTMLElement | null {
+ for (const selector of CHECKOUT_BUTTON_SELECTORS) {
+ const el = document.querySelector(selector);
+ if (el) return el;
+ }
+ return null;
+}
+
+function isCartPage(): boolean {
+ // Matches /cart, /cart/, and locale-prefixed variants like /en/cart —
+ // but not /cart/add or similar sub-paths that aren't the cart page itself.
+ return window.location.pathname.replace(/\/+$/, "").endsWith("/cart");
+}
+
+function maybeAutoPlaceOnCart() {
+ if (!isCartPage()) return;
+ if (document.querySelector("[data-dd-widget]")) return; // a block is already placed manually — don't double up
+
+ const template = document.getElementById("dd-widget-cart-template");
+ if (!template) return; // merchant turned auto-placement off in the app embed's settings
+
+ const checkoutButton = findCheckoutButton();
+ if (!checkoutButton) return; // couldn't find a safe, theme-agnostic anchor — do nothing rather than guess
+
+ const widget = document.createElement("div");
+ widget.setAttribute("data-dd-widget", "");
+ widget.classList.add("dd-widget--cart-injected");
+ for (const attr of Array.from(template.attributes)) {
+ if (attr.name === "id") continue;
+ widget.setAttribute(attr.name, attr.value);
+ }
+
+ checkoutButton.insertAdjacentElement("beforebegin", widget);
+ new DateTimeWidget(readConfig(widget)).mount();
+}
+
function init() {
const roots = document.querySelectorAll("[data-dd-widget]");
roots.forEach((root) => {
new DateTimeWidget(readConfig(root)).mount();
});
+ maybeAutoPlaceOnCart();
}
if (document.readyState === "loading") {