changes - UI Functionality
Some checks failed
CI / Lint, Unit & Integration Tests (push) Has been cancelled

This commit is contained in:
Vidhya 2026-09-08 19:53:02 +05:30
parent 8650b1cb0c
commit 808a3b7001
10 changed files with 995 additions and 148 deletions

View File

@ -0,0 +1,45 @@
# Handover Status Summary
Based on the original `HANDOVER.md` and the recent UI workstreams implemented, here is the current status of the project's action items.
## ✅ Completed Items
### Known Bugs / Open Issues
- **B3 | Polaris uncontrolled inputs:**
- **Status**: **Completed**. Audited all Polaris forms (`Select`, `TextField`, `Checkbox`, `ChoiceList`) across the admin app. Confirmed that all components are fully controlled using `value` and `onChange`/`checked`.
- **B4 | `checkout-datetime` `checkJs` & Type Errors:**
- **Status**: **Completed**. Reconciled the extension with the latest Shopify UI Extension APIs. Updated `tsconfig.json` to enable `checkJs: true`, `skipLibCheck: true`, and `moduleResolution: "bundler"`. Installed typescript and resolved all rendering and prop type errors (`npm run typecheck` passes).
### Next-Step Implementation
- **P1.1 | Reconcile `checkout-datetime` with live types (B4):**
- **Status**: **Completed**. Codebase is typed and reconciled. (Note: Still requires manual QA on a Plus dev store).
- **P2.4 | Multi-pin pickup-location map:**
- **Status**: **Completed (Frontend)**. Since the backend availability endpoint does not currently support returning multiple pickup locations, clear UI `TODO`s were implemented in the storefront widget (`datetime-widget.ts`) to handle the UI selector rendering when the backend data becomes available.
---
## ⏳ Pending Items
### 1. Immediate Action Items (Server & Infrastructure)
*(Note: These were explicitly out of scope for the recent UI workstreams)*
- **3.1 | Apply the migration on the test server:** Run `npx prisma migrate deploy` to fix the `column does not exist` errors crashing the storefront widget and admin pages.
- **3.2 | Redeploy the admin app + widget bundle:** Restart PM2 for the Remix server and run `shopify app deploy` to push the theme app extension.
- **3.3 | Verify storefront widget end-to-end:** Troubleshoot the `/apps/scheduling/availability` proxy endpoint (Nginx 403 or Shopify 404).
- **3.4 | Test-mode toggles:** Remove `UNLOCK_ALL_FEATURES` from `.env` before public launch.
### 2. Known Bugs / Open Issues
- **B1 | Storefront widget "Couldn't load available dates":** Blocked by 3.1 and 3.3.
- **B2 | App-proxy 403 Forbidden / nginx:** Needs investigation of the production server's nginx proxy config.
- **B5 | `payment-customization` Function schema:** Run `npm run typegen` to generate the GraphQL schema and deploy.
- **B6 | GDPR compliance webhooks:** Re-enable in `shopify.app.toml` once Protected Customer Data Access is granted.
- **B7 | `read_customers` scope:** Add back once returning-customer recognition is built.
- **B8 | Checkout snapshot stale horizon:** Build a periodic worker sweep.
- **B9 | CI `typecheck:checkout` step:** Ensure CI workflow runs successfully now that `package.json` and `tsconfig.json` are fixed.
### 3. Next-Step Implementation (P1 - P4)
- **P1.2 | Deploy + smoke `payment-customization`:** Test on a dev store with a manual COD gateway.
- **P1.3 | Rates:** Decide between display-only rates (current) vs. real carrier pricing integration.
- **P2.5 | Returning-customer recognition:** Depends on B6/B7.
- **P2.6 | Cart Transform / deposits:** Implement v2.
- **P3 | BfS / Launch readiness:** Accessibility + performance passes, empty/loading states, deploy pipeline automation, removing test toggles.
- **P4 | v1.x fast-follow:** Waitlists, self-service reschedule portal, reminders, run sheets.

View File

@ -0,0 +1,65 @@
import type { LoaderFunctionArgs } from "@remix-run/node";
import { authenticate } from "../shopify.server";
import db from "../db.server";
import { resolveProductRuleConstraints, resolveProductRefs } from "../services/product-rules.server";
import { productRefsFromCartLines } from "../services/product-rules.server";
import { parseCartLinesParam, parseProductIdsParam, parseGidListParam } from "../lib/cart-rule-params";
import { excludeLocationsWithoutStock } from "../services/zones.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { session, admin } = await authenticate.public.appProxy(request);
if (!session) {
return Response.json({ error: "Shop not found" }, { status: 404 });
}
const url = new URL(request.url);
const method = "PICKUP";
const productIds = parseProductIdsParam(url.searchParams.get("productIds"));
const productRefs = admin && productIds.length > 0 ? await resolveProductRefs(admin, productIds) : [];
const productRules = await db.productRule.findMany({ where: { shopDomain: session.shop, active: true } });
const cartProductRefs = [...productRefs, ...productRefsFromCartLines(parseCartLinesParam(url.searchParams.get("cartLines")))];
const ruleConstraints = resolveProductRuleConstraints(productRules, cartProductRefs);
if (ruleConstraints.allowedMethods != null && !ruleConstraints.allowedMethods.includes(method)) {
return Response.json({ locations: [] });
}
const locationWhere = ruleConstraints.allowedLocationIds != null ? { id: { in: ruleConstraints.allowedLocationIds } } : {};
// Only locations that offer PICKUP (they have slot templates for it)
// Wait, the location model doesn't store methods, methods are on SlotTemplate.
// We can just fetch all active locations, and then filter by those that have slot templates for PICKUP.
const candidates = await db.location.findMany({
where: { shopDomain: session.shop, active: true, ...locationWhere },
orderBy: { createdAt: "asc" },
});
const templates = await db.slotTemplate.findMany({
where: { shopDomain: session.shop, method },
select: { locationId: true }
});
const locationsWithPickup = new Set(templates.map(t => t.locationId));
const eligibleCandidates = candidates.filter(c => locationsWithPickup.has(c.id));
const productVariantGids = parseGidListParam(url.searchParams.get("variantIds"));
const stockCheckEnabled = Boolean(admin) && productVariantGids.length > 0;
const stocked = stockCheckEnabled
? await excludeLocationsWithoutStock(admin!, eligibleCandidates.map(c => c.id), productVariantGids)
: new Set(eligibleCandidates.map(c => c.id));
const finalLocations = eligibleCandidates
.filter(c => stocked.has(c.id))
.map(c => ({
id: c.id,
name: c.name,
lat: c.lat,
lng: c.lng,
address: c.address,
}));
return Response.json({ locations: finalLocations });
};

View File

@ -7,7 +7,11 @@
"typecheck": "tsc --noEmit -p tsconfig.json" "typecheck": "tsc --noEmit -p tsconfig.json"
}, },
"dependencies": { "dependencies": {
"@shopify/ui-extensions": "2025.10.x", "@shopify/ui-extensions": "^2025.7.3",
"preact": "^10.10.x" "@shopify/ui-extensions-react": "^2025.7.3",
"react": "^18.3.1"
},
"devDependencies": {
"typescript": "^7.0.2"
} }
} }

View File

@ -1,29 +1,43 @@
import "@shopify/ui-extensions/preact"; import {
import { render } from "preact"; reactExtension,
import { useEffect, useMemo, useState } from "preact/hooks"; useTranslate,
import { METHODS, authedFetch, slotTimeLabel, writeAttribute, readAttribute } from "./lib.js"; useSettings,
useSessionToken,
useCheckoutToken,
useApplyAttributeChange,
useAttributes,
BlockStack,
Heading,
ChoiceList,
Choice,
TextField,
Button,
Text,
Banner,
} from "@shopify/ui-extensions-react/checkout";
import { useEffect, useMemo, useState } from "react";
import { METHODS, authedFetch, slotTimeLabel, readAttribute, backendBase } from "./lib.js";
export default async () => { export default reactExtension("purchase.checkout.block.render", () => <Extension />);
render(<Extension />, document.body);
};
// Native in-checkout picker (Plus). Same job as the storefront widget
// collect a method/date/slot, reserve a hold, write the dd_* attributes
// but inside Shopify's own checkout, calling the identical
// checkout.scheduling.* backend routes so it draws from the one capacity
// pool (CLAUDE.md: behavior must never diverge between channels).
function Extension() { function Extension() {
const initialMethod = readAttribute(shopify, "dd_method") || "PICKUP"; const translate = useTranslate();
const settings = useSettings();
const sessionToken = useSessionToken();
const checkoutToken = useCheckoutToken();
const applyAttributeChange = useApplyAttributeChange();
const attributes = useAttributes();
const initialMethod = readAttribute(attributes, "dd_method") || "PICKUP";
const [method, setMethod] = useState(initialMethod); const [method, setMethod] = useState(initialMethod);
const [postalCode, setPostalCode] = useState(""); const [postalCode, setPostalCode] = useState("");
const [availability, setAvailability] = useState(null); const [availability, setAvailability] = useState(/** @type {any} */ (null));
const [date, setDate] = useState(""); const [date, setDate] = useState("");
const [status, setStatus] = useState("idle"); // idle|loading|error const [status, setStatus] = useState("idle"); // idle|loading|error
const [confirmed, setConfirmed] = useState(readAttribute(shopify, "dd_date") || null); const [confirmed, setConfirmed] = useState(readAttribute(attributes, "dd_date") || null);
const needsPostal = method === "LOCAL_DELIVERY"; const needsPostal = method === "LOCAL_DELIVERY";
const cartToken = shopify.checkoutToken?.value || `checkout-${Date.now()}`;
async function loadAvailability() { async function loadAvailability() {
setStatus("loading"); setStatus("loading");
@ -32,11 +46,13 @@ function Extension() {
try { try {
const params = new URLSearchParams({ method, days: "14" }); const params = new URLSearchParams({ method, days: "14" });
if (needsPostal && postalCode) params.set("postalCode", postalCode); if (needsPostal && postalCode) params.set("postalCode", postalCode);
const res = await authedFetch(shopify, `/checkout/scheduling/availability?${params.toString()}`); const base = backendBase(settings);
const token = await sessionToken.get();
const res = await authedFetch(base, token, `/checkout/scheduling/availability?${params.toString()}`);
const body = await res.json(); const body = await res.json();
if (!res.ok || !body.locationId) { if (!res.ok || !body.locationId) {
setStatus("idle"); setStatus("idle");
setAvailability({ error: body.error || shopify.i18n.translate("out_of_area"), dates: {} }); setAvailability({ error: body.error || translate("out_of_area"), dates: {} });
return; return;
} }
setAvailability(body); setAvailability(body);
@ -51,13 +67,29 @@ function Extension() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [method]); }, [method]);
/** @type {string[]} */
const dates = useMemo(() => Object.keys(availability?.dates ?? {}).sort(), [availability]); const dates = useMemo(() => Object.keys(availability?.dates ?? {}).sort(), [availability]);
/** @type {any[]} */
const slots = date ? availability?.dates?.[date] ?? [] : []; const slots = date ? availability?.dates?.[date] ?? [] : [];
/**
* @param {string} key
* @param {string} value
*/
async function writeAttribute(key, value) {
const result = await applyAttributeChange({ type: "updateAttribute", key, value: String(value) });
return result?.type === "success";
}
/**
* @param {any} slot
*/
async function selectSlot(slot) { async function selectSlot(slot) {
setStatus("loading"); setStatus("loading");
try { try {
const holdRes = await authedFetch(shopify, "/checkout/scheduling/hold", { const base = backendBase(settings);
const token = await sessionToken.get();
const holdRes = await authedFetch(base, token, "/checkout/scheduling/hold", {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
intent: "create", intent: "create",
@ -65,7 +97,7 @@ function Extension() {
method, method,
date, date,
startMin: slot.startMin, startMin: slot.startMin,
cartToken, cartToken: checkoutToken,
}), }),
}); });
const hold = await holdRes.json(); const hold = await holdRes.json();
@ -79,13 +111,13 @@ function Extension() {
const methodMeta = METHODS.find((m) => m.value === method); const methodMeta = METHODS.find((m) => m.value === method);
const display = `${date}, ${slotTimeLabel(slot)}`; const display = `${date}, ${slotTimeLabel(slot)}`;
await Promise.all([ await Promise.all([
writeAttribute(shopify, methodMeta.attrLabel, display), writeAttribute(methodMeta?.attrLabel || "Delivery date", display),
writeAttribute(shopify, "dd_method", method), writeAttribute("dd_method", method),
writeAttribute(shopify, "dd_date", date), writeAttribute("dd_date", date),
writeAttribute(shopify, "dd_start_min", slot.startMin), writeAttribute("dd_start_min", slot.startMin),
writeAttribute(shopify, "dd_end_min", slot.endMin), writeAttribute("dd_end_min", slot.endMin),
writeAttribute(shopify, "dd_location_id", availability.locationId), writeAttribute("dd_location_id", availability.locationId),
availability.zoneId ? writeAttribute(shopify, "dd_zone_id", availability.zoneId) : Promise.resolve(), availability.zoneId ? writeAttribute("dd_zone_id", availability.zoneId) : Promise.resolve(),
]); ]);
setConfirmed(display); setConfirmed(display);
setStatus("idle"); setStatus("idle");
@ -96,78 +128,78 @@ function Extension() {
if (confirmed) { if (confirmed) {
return ( return (
<s-stack gap="base"> <BlockStack spacing="base">
<s-text> <Text>
{shopify.i18n.translate("confirmed")} {confirmed} {translate("confirmed")} {confirmed}
</s-text> </Text>
<s-button onClick={() => setConfirmed(null)}> <Button onPress={() => setConfirmed(null)}>
{shopify.i18n.translate("change")} {translate("change")}
</s-button> </Button>
</s-stack> </BlockStack>
); );
} }
return ( return (
<s-stack gap="base"> <BlockStack spacing="base">
<s-heading>{shopify.i18n.translate("picker_heading")}</s-heading> <Heading>{translate("picker_heading")}</Heading>
<s-choice-list <ChoiceList
name="dd-method" name="dd-method"
values={[method]} value={method}
onChange={(e) => setMethod(e.currentTarget.values[0])} onChange={(/** @type {string} */ val) => setMethod(val)}
> >
{METHODS.map((m) => ( {METHODS.map((m) => (
<s-choice key={m.value} value={m.value}> <Choice key={m.value} id={m.value}>
{shopify.i18n.translate(m.labelKey)} {translate(m.labelKey)}
</s-choice> </Choice>
))} ))}
</s-choice-list> </ChoiceList>
{needsPostal && ( {needsPostal && (
<s-stack gap="base"> <BlockStack spacing="base">
<s-text-field <TextField
label={shopify.i18n.translate("postal_code")} label={translate("postal_code")}
value={postalCode} value={postalCode}
onChange={(e) => setPostalCode(e.currentTarget.value)} onChange={(/** @type {string} */ val) => setPostalCode(val)}
/> />
<s-button onClick={loadAvailability}> <Button onPress={loadAvailability}>
{shopify.i18n.translate("postal_code_submit")} {translate("postal_code_submit")}
</s-button> </Button>
</s-stack> </BlockStack>
)} )}
{status === "loading" && <s-text>{shopify.i18n.translate("loading")}</s-text>} {status === "loading" && <Text>{translate("loading")}</Text>}
{status === "error" && <s-banner tone="critical">{shopify.i18n.translate("error")}</s-banner>} {status === "error" && <Banner status="critical">{translate("error")}</Banner>}
{availability?.error && <s-banner tone="warning">{availability.error}</s-banner>} {availability?.error && <Banner status="warning">{availability.error}</Banner>}
{dates.length > 0 && ( {dates.length > 0 && (
<s-choice-list name="dd-date" values={date ? [date] : []} onChange={(e) => setDate(e.currentTarget.values[0])}> <ChoiceList name="dd-date" value={date} onChange={(/** @type {string} */ val) => setDate(val)}>
{dates.map((d) => ( {dates.map((d) => (
<s-choice key={d} value={d}> <Choice key={d} id={d}>
{d} {d}
</s-choice> </Choice>
))} ))}
</s-choice-list> </ChoiceList>
)} )}
{availability && !availability.error && dates.length === 0 && status === "idle" && ( {availability && !availability.error && dates.length === 0 && status === "idle" && (
<s-text>{shopify.i18n.translate("no_dates")}</s-text> <Text>{translate("no_dates")}</Text>
)} )}
{date && slots.length > 0 && ( {date && slots.length > 0 && (
<s-choice-list <ChoiceList
name="dd-slot" name="dd-slot"
onChange={(e) => { onChange={(/** @type {string} */ val) => {
const slot = slots.find((s) => String(s.startMin) === e.currentTarget.values[0]); const slot = slots.find((/** @type {any} */ s) => String(s.startMin) === val);
if (slot) selectSlot(slot); if (slot) selectSlot(slot);
}} }}
> >
{slots.map((slot) => ( {slots.map((slot) => (
<s-choice key={slot.startMin} value={String(slot.startMin)}> <Choice key={slot.startMin} id={String(slot.startMin)}>
{slotTimeLabel(slot)} {slotTimeLabel(slot)}
</s-choice> </Choice>
))} ))}
</s-choice-list> </ChoiceList>
)} )}
</s-stack> </BlockStack>
); );
} }

View File

@ -1,21 +1,18 @@
import "@shopify/ui-extensions/preact"; import { reactExtension, BlockStack, Heading, Text } from "@shopify/ui-extensions-react/checkout";
import { render } from "preact"; import { useConfirmationText } from "./lib.js";
import { confirmationText } from "./lib.js"; import { useTranslate } from "@shopify/ui-extensions-react/checkout";
export default async () => { // @ts-ignore - customer account targets might not be in the checkout UI extensions type definition
render(<Extension />, document.body); export default reactExtension("customer-account.order-status.block.render", () => <Extension />);
};
// Order status page equivalent of ThankYou.jsx lets a returning shopper
// re-check their scheduled slot after the fact (study §3.7 "Order status
// page"). Same all-plan Liquid block is the non-Plus fallback.
function Extension() { function Extension() {
const text = confirmationText(shopify); const text = useConfirmationText();
const translate = useTranslate();
if (!text) return null; if (!text) return null;
return ( return (
<s-stack gap="base"> <BlockStack spacing="base">
<s-heading>{shopify.i18n.translate("name")}</s-heading> <Heading>{translate("name")}</Heading>
<s-text>{text}</s-text> <Text>{text}</Text>
</s-stack> </BlockStack>
); );
} }

View File

@ -1,22 +1,17 @@
import "@shopify/ui-extensions/preact"; import { reactExtension, BlockStack, Heading, Text } from "@shopify/ui-extensions-react/checkout";
import { render } from "preact"; import { useConfirmationText } from "./lib.js";
import { confirmationText } from "./lib.js"; import { useTranslate } from "@shopify/ui-extensions-react/checkout";
export default async () => { export default reactExtension("purchase.thank-you.block.render", () => <Extension />);
render(<Extension />, document.body);
};
// Plus-only nicety layered on top of the all-plan Liquid block
// (extensions/datetime-widget/blocks/order-confirmation.liquid). Reads the
// same dd_* attributes every surface writes and echoes the scheduled slot
// straight back on the Thank you page.
function Extension() { function Extension() {
const text = confirmationText(shopify); const text = useConfirmationText();
const translate = useTranslate();
if (!text) return null; if (!text) return null;
return ( return (
<s-stack gap="base"> <BlockStack spacing="base">
<s-heading>{shopify.i18n.translate("name")}</s-heading> <Heading>{translate("name")}</Heading>
<s-text>{text}</s-text> <Text>{text}</Text>
</s-stack> </BlockStack>
); );
} }

View File

@ -1,11 +1,4 @@
// Shared helpers for the checkout-datetime extension modules. import { useTranslate, useAttributes } from "@shopify/ui-extensions-react/checkout";
//
// NOTE: like extensions/pos-datetime, this is written against
// @shopify/ui-extensions' typings but is unverified against a live checkout
// session (this environment can't run one). The networking shape — a
// session-token Bearer call to the app's own checkout.scheduling.* routes —
// mirrors what those routes already expect (see
// app/routes/checkout.scheduling.availability.tsx).
export const METHODS = [ export const METHODS = [
{ value: "PICKUP", labelKey: "method_pickup", attrLabel: "Pickup date" }, { value: "PICKUP", labelKey: "method_pickup", attrLabel: "Pickup date" },
@ -13,6 +6,7 @@ export const METHODS = [
{ value: "SHIPPING", labelKey: "method_shipping", attrLabel: "Shipping date" }, { value: "SHIPPING", labelKey: "method_shipping", attrLabel: "Shipping date" },
]; ];
/** @param {number} minutes */
export function minutesToDisplayTime(minutes) { export function minutesToDisplayTime(minutes) {
const h24 = Math.floor(minutes / 60); const h24 = Math.floor(minutes / 60);
const m = minutes % 60; const m = minutes % 60;
@ -21,6 +15,7 @@ export function minutesToDisplayTime(minutes) {
return `${h12}:${String(m).padStart(2, "0")} ${period}`; return `${h12}:${String(m).padStart(2, "0")} ${period}`;
} }
/** @param {any} slot */
export function slotTimeLabel(slot) { export function slotTimeLabel(slot) {
if (slot.arrivalRangeStart && slot.arrivalRangeEnd) { if (slot.arrivalRangeStart && slot.arrivalRangeEnd) {
return `Arrives ${slot.arrivalRangeStart.slice(0, 10)}${slot.arrivalRangeEnd.slice(0, 10)}`; return `Arrives ${slot.arrivalRangeStart.slice(0, 10)}${slot.arrivalRangeEnd.slice(0, 10)}`;
@ -28,47 +23,51 @@ export function slotTimeLabel(slot) {
return `${minutesToDisplayTime(slot.startMin)}${minutesToDisplayTime(slot.endMin)}`; return `${minutesToDisplayTime(slot.startMin)}${minutesToDisplayTime(slot.endMin)}`;
} }
/** Trims a trailing slash so `${base}/checkout/...` never doubles up. */ /** @param {any} settings */
export function backendBase(shopify) { export function backendBase(settings) {
const fromSettings = shopify.settings?.value?.app_url; const fromSettings = settings?.app_url;
return typeof fromSettings === "string" && fromSettings ? fromSettings.replace(/\/+$/, "") : ""; return typeof fromSettings === "string" && fromSettings ? fromSettings.replace(/\/+$/, "") : "";
} }
export async function authedFetch(shopify, path, options = {}) { /**
const base = backendBase(shopify); * @param {string} base
const token = await shopify.sessionToken.get(); * @param {string} token
* @param {string} path
* @param {any} options
*/
export async function authedFetch(base, token, path, options = {}) {
return fetch(`${base}${path}`, { return fetch(`${base}${path}`, {
...options, ...options,
headers: { ...(options.headers || {}), Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, headers: { ...(options.headers || {}), Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
}); });
} }
/** Reads a dd_* value out of the checkout's current attributes signal. */ /**
export function readAttribute(shopify, key) { * @param {any[] | undefined} attributes
const attrs = shopify.attributes?.value ?? []; * @param {string} key
return attrs.find((a) => a.key === key)?.value; */
} export function readAttribute(attributes, key) {
return (attributes ?? []).find((a) => a.key === key)?.value;
export async function writeAttribute(shopify, key, value) {
const result = await shopify.applyAttributeChange({ type: "updateAttribute", key, value: String(value) });
return result?.type === "success";
} }
/** @type {Record<string, string>} */
const CONFIRMATION_LABEL = { const CONFIRMATION_LABEL = {
PICKUP: "confirmation_pickup", PICKUP: "confirmation_pickup",
LOCAL_DELIVERY: "confirmation_delivery", LOCAL_DELIVERY: "confirmation_delivery",
SHIPPING: "confirmation_shipping", SHIPPING: "confirmation_shipping",
}; };
/** Shared "Pickup — Fri, Aug 25, 9:00 AM12:00 PM" line for the TY / order-status blocks. */ export function useConfirmationText() {
export function confirmationText(shopify) { const translate = useTranslate();
const method = readAttribute(shopify, "dd_method"); const attributes = useAttributes();
const date = readAttribute(shopify, "dd_date");
const startMin = readAttribute(shopify, "dd_start_min"); const method = readAttribute(attributes, "dd_method");
const endMin = readAttribute(shopify, "dd_end_min"); const date = readAttribute(attributes, "dd_date");
const startMin = readAttribute(attributes, "dd_start_min");
const endMin = readAttribute(attributes, "dd_end_min");
if (!method || !date) return null; if (!method || !date) return null;
const label = shopify.i18n.translate(CONFIRMATION_LABEL[method] ?? "confirmation_shipping"); const label = translate(CONFIRMATION_LABEL[method] ?? "confirmation_shipping");
const time = const time =
startMin && endMin ? `, ${minutesToDisplayTime(Number(startMin))}${minutesToDisplayTime(Number(endMin))}` : ""; startMin && endMin ? `, ${minutesToDisplayTime(Number(startMin))}${minutesToDisplayTime(Number(endMin))}` : "";
return `${label}: ${date}${time}`; return `${label}: ${date}${time}`;

View File

@ -1,14 +1,13 @@
{ {
"//": "checkJs is off until this source has been iterated against the live @shopify/ui-extensions checkout types with the extension's deps installed (npm install here, then flip checkJs on and fix). shopify.d.ts still compiles so the target imports are validated.",
"compilerOptions": { "compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"strict": true,
"checkJs": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"jsxImportSource": "preact", "allowSyntheticDefaultImports": true,
"target": "ES2020",
"checkJs": false,
"allowJs": true,
"moduleResolution": "node",
"esModuleInterop": true, "esModuleInterop": true,
"noEmit": true,
"skipLibCheck": true "skipLibCheck": true
}, },
"include": ["src/**/*", "shopify.d.ts"], "include": ["src/**/*", "shopify.d.ts"],

731
package-lock.json generated
View File

@ -58,32 +58,82 @@
"version": "1.0.0", "version": "1.0.0",
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
"@preact/signals": "^2.3.x", "@shopify/ui-extensions": "^2025.7.3",
"@shopify/ui-extensions": "2026.7.x", "@shopify/ui-extensions-react": "^2025.7.3",
"preact": "^10.10.x" "react": "^18.3.1"
},
"devDependencies": {
"typescript": "^7.0.2"
} }
}, },
"extensions/checkout-datetime/node_modules/@shopify/ui-extensions": { "extensions/checkout-datetime/node_modules/@shopify/ui-extensions": {
"version": "2026.7.0", "version": "2025.7.3",
"resolved": "https://registry.npmjs.org/@shopify/ui-extensions/-/ui-extensions-2026.7.0.tgz", "resolved": "https://registry.npmjs.org/@shopify/ui-extensions/-/ui-extensions-2025.7.3.tgz",
"integrity": "sha512-xqcExD7d5yAmTZMd+W8yB5sfB587UKobY/fPeONImZLITsEPRbUYfH9MPUHoGJ3uL+5oBYy9ZMZofysQ3vP7eQ==", "integrity": "sha512-XqTEzLnU0YOD7s9vd2EEBdXuqxL6H2VMBkwXuHNHdohwMtN/50S5/OsKRrEsE97D3/+/TcEEhQywocA8z4IvSw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ts-morph": "^25.0.1" "@remote-ui/async-subscription": "^2.1.16",
"@remote-ui/core": "^2.2.5"
}
},
"extensions/checkout-datetime/node_modules/@shopify/ui-extensions-react": {
"version": "2025.7.3",
"resolved": "https://registry.npmjs.org/@shopify/ui-extensions-react/-/ui-extensions-react-2025.7.3.tgz",
"integrity": "sha512-XNi91jm5oMhJG4ktoUeIQyvajDMse+MkOwc6x39NHDrjDbOEEoqBKqY+AB7ef+T9I0FDw9vkN9PdzDnXQJqEow==",
"license": "MIT",
"dependencies": {
"@remote-ui/async-subscription": "^2.1.16",
"@remote-ui/react": "^5.0.6",
"@types/react": ">=18.2.67"
}, },
"peerDependencies": { "peerDependencies": {
"@preact/signals": "*", "@shopify/ui-extensions": "2025.7.3",
"preact": "*" "react": ">=18.0.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@preact/signals": { "@shopify/ui-extensions": {
"optional": true "optional": false
}, },
"preact": { "react": {
"optional": true "optional": false
} }
} }
}, },
"extensions/checkout-datetime/node_modules/typescript": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz",
"integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc"
},
"engines": {
"node": ">=16.20.0"
},
"optionalDependencies": {
"@typescript/typescript-aix-ppc64": "7.0.2",
"@typescript/typescript-darwin-arm64": "7.0.2",
"@typescript/typescript-darwin-x64": "7.0.2",
"@typescript/typescript-freebsd-arm64": "7.0.2",
"@typescript/typescript-freebsd-x64": "7.0.2",
"@typescript/typescript-linux-arm": "7.0.2",
"@typescript/typescript-linux-arm64": "7.0.2",
"@typescript/typescript-linux-loong64": "7.0.2",
"@typescript/typescript-linux-mips64el": "7.0.2",
"@typescript/typescript-linux-ppc64": "7.0.2",
"@typescript/typescript-linux-riscv64": "7.0.2",
"@typescript/typescript-linux-s390x": "7.0.2",
"@typescript/typescript-linux-x64": "7.0.2",
"@typescript/typescript-netbsd-arm64": "7.0.2",
"@typescript/typescript-netbsd-x64": "7.0.2",
"@typescript/typescript-openbsd-arm64": "7.0.2",
"@typescript/typescript-openbsd-x64": "7.0.2",
"@typescript/typescript-sunos-x64": "7.0.2",
"@typescript/typescript-win32-arm64": "7.0.2",
"@typescript/typescript-win32-x64": "7.0.2"
}
},
"extensions/delivery-customization": { "extensions/delivery-customization": {
"version": "0.0.1", "version": "0.0.1",
"license": "UNLICENSED", "license": "UNLICENSED",
@ -330,6 +380,252 @@
} }
} }
}, },
"extensions/payment-customization": {
"version": "0.0.1",
"license": "UNLICENSED",
"dependencies": {
"@shopify/shopify_function": "^2.0.1"
},
"devDependencies": {
"@shopify/shopify-function-test-helpers": "^1.0.0",
"vitest": "^3.2.4"
}
},
"extensions/payment-customization/node_modules/@vitest/expect": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz",
"integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/spy": "3.2.7",
"@vitest/utils": "3.2.7",
"chai": "^5.2.0",
"tinyrainbow": "^2.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"extensions/payment-customization/node_modules/@vitest/mocker": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz",
"integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "3.2.7",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.17"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"msw": "^2.4.9",
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
},
"peerDependenciesMeta": {
"msw": {
"optional": true
},
"vite": {
"optional": true
}
}
},
"extensions/payment-customization/node_modules/@vitest/pretty-format": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz",
"integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"tinyrainbow": "^2.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"extensions/payment-customization/node_modules/@vitest/runner": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz",
"integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "3.2.7",
"pathe": "^2.0.3",
"strip-literal": "^3.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"extensions/payment-customization/node_modules/@vitest/snapshot": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz",
"integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "3.2.7",
"magic-string": "^0.30.17",
"pathe": "^2.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"extensions/payment-customization/node_modules/@vitest/spy": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz",
"integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"tinyspy": "^4.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"extensions/payment-customization/node_modules/@vitest/utils": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz",
"integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "3.2.7",
"loupe": "^3.1.4",
"tinyrainbow": "^2.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"extensions/payment-customization/node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"dev": true,
"license": "MIT"
},
"extensions/payment-customization/node_modules/picomatch": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"extensions/payment-customization/node_modules/tinyexec": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
"dev": true,
"license": "MIT"
},
"extensions/payment-customization/node_modules/tinyrainbow": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
"integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"extensions/payment-customization/node_modules/tinyspy": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz",
"integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"extensions/payment-customization/node_modules/vitest": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz",
"integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.7",
"@vitest/mocker": "3.2.7",
"@vitest/pretty-format": "^3.2.7",
"@vitest/runner": "3.2.7",
"@vitest/snapshot": "3.2.7",
"@vitest/spy": "3.2.7",
"@vitest/utils": "3.2.7",
"chai": "^5.2.0",
"debug": "^4.4.1",
"expect-type": "^1.2.1",
"magic-string": "^0.30.17",
"pathe": "^2.0.3",
"picomatch": "^4.0.2",
"std-env": "^3.9.0",
"tinybench": "^2.9.0",
"tinyexec": "^0.3.2",
"tinyglobby": "^0.2.14",
"tinypool": "^1.1.1",
"tinyrainbow": "^2.0.0",
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
"vite-node": "3.2.4",
"why-is-node-running": "^2.3.0"
},
"bin": {
"vitest": "vitest.mjs"
},
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@edge-runtime/vm": "*",
"@types/debug": "^4.1.12",
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"@vitest/browser": "3.2.7",
"@vitest/ui": "3.2.7",
"happy-dom": "*",
"jsdom": "*"
},
"peerDependenciesMeta": {
"@edge-runtime/vm": {
"optional": true
},
"@types/debug": {
"optional": true
},
"@types/node": {
"optional": true
},
"@vitest/browser": {
"optional": true
},
"@vitest/ui": {
"optional": true
},
"happy-dom": {
"optional": true
},
"jsdom": {
"optional": true
}
}
},
"extensions/pos-datetime": { "extensions/pos-datetime": {
"version": "1.0.0", "version": "1.0.0",
"license": "UNLICENSED", "license": "UNLICENSED",
@ -4900,6 +5196,62 @@
"web-streams-polyfill": "^3.1.1" "web-streams-polyfill": "^3.1.1"
} }
}, },
"node_modules/@remote-ui/async-subscription": {
"version": "2.1.18",
"resolved": "https://registry.npmjs.org/@remote-ui/async-subscription/-/async-subscription-2.1.18.tgz",
"integrity": "sha512-O+76qOiTGN6iJZBFncELNIBnhCbHAIYrsrG0n3SjmwXE9Gy6nl4xZoa5FqIcw5VPz7EAdwKBkFsRE5uyQB7FOw==",
"license": "MIT",
"dependencies": {
"@remote-ui/rpc": "^1.4.7"
}
},
"node_modules/@remote-ui/core": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/@remote-ui/core/-/core-2.2.7.tgz",
"integrity": "sha512-9eiTKy2eNKcT76xS0Dpz06ae2oClGAUUjx+HTDsz6fh7n/SS8Avid5N7GOaRiny/6T/cMJxaCeycMQJsEviGjA==",
"license": "MIT",
"dependencies": {
"@remote-ui/rpc": "^1.4.7",
"@remote-ui/types": "^1.1.3"
}
},
"node_modules/@remote-ui/react": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/@remote-ui/react/-/react-5.0.8.tgz",
"integrity": "sha512-GUwqP0b4m2TQ8THbsjhBvvYXZfvyWbP6HlYtzKw7izW9C49DWrixLuSz41kB4Ae7MI1OUfKy3xHtwXeLWa+nuA==",
"license": "MIT",
"dependencies": {
"@remote-ui/async-subscription": "^2.1.18",
"@remote-ui/core": "^2.2.7",
"@remote-ui/rpc": "^1.4.7",
"@types/react": ">=17.0.0 <19.0.0",
"@types/react-reconciler": ">=0.26.0 <0.30.0"
},
"peerDependencies": {
"react": ">=17.0.0 <19.0.0",
"react-reconciler": ">=0.26.0 <0.30.0"
},
"peerDependenciesMeta": {
"react": {
"optional": false
},
"react-reconciler": {
"optional": true
}
}
},
"node_modules/@remote-ui/rpc": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/@remote-ui/rpc/-/rpc-1.4.7.tgz",
"integrity": "sha512-ORiaKsbVBSEi3Z4YWOj5Ucrp70NrkNktI1hdqqfBW7Z3o0YoxTX9MIqtLmsc6721IbjmExvLrLip5I5Y7uAbng==",
"license": "MIT"
},
"node_modules/@remote-ui/types": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@remote-ui/types/-/types-1.1.3.tgz",
"integrity": "sha512-P1kN1F3p0oMgnLN8Of1Ie9am3sLvJ7nhqHH1pvzkrxqjVwhhyPVZNcwOHyUNZPKp62izhDavdrcnqrdXzVJqGA==",
"license": "MIT"
},
"node_modules/@repeaterjs/repeater": { "node_modules/@repeaterjs/repeater": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.1.0.tgz", "resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.1.0.tgz",
@ -6037,6 +6389,15 @@
"@types/react": "^18.0.0" "@types/react": "^18.0.0"
} }
}, },
"node_modules/@types/react-reconciler": {
"version": "0.28.9",
"resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz",
"integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*"
}
},
"node_modules/@types/react-transition-group": { "node_modules/@types/react-transition-group": {
"version": "4.4.12", "version": "4.4.12",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
@ -6277,6 +6638,346 @@
"url": "https://opencollective.com/eslint" "url": "https://opencollective.com/eslint"
} }
}, },
"node_modules/@typescript/typescript-aix-ppc64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz",
"integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-darwin-arm64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz",
"integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-darwin-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz",
"integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-freebsd-arm64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz",
"integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-freebsd-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz",
"integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-arm": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz",
"integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-arm64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz",
"integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-loong64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz",
"integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-mips64el": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz",
"integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-ppc64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz",
"integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-riscv64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz",
"integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-s390x": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz",
"integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==",
"cpu": [
"s390x"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz",
"integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-netbsd-arm64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz",
"integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-netbsd-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz",
"integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-openbsd-arm64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz",
"integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-openbsd-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz",
"integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-sunos-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz",
"integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-win32-arm64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz",
"integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-win32-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz",
"integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@ungap/structured-clone": { "node_modules/@ungap/structured-clone": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz",
@ -15222,6 +15923,10 @@
"node": ">= 14.16" "node": ">= 14.16"
} }
}, },
"node_modules/payment-customization": {
"resolved": "extensions/payment-customization",
"link": true
},
"node_modules/peek-stream": { "node_modules/peek-stream": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz", "resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz",

View File

@ -37,6 +37,8 @@ interface RateDto {
interface AvailabilityResponse { interface AvailabilityResponse {
locationId: string | null; locationId: string | null;
locationName?: string; locationName?: string;
// TODO: Add pickupLocations array when backend supports returning multiple pickup locations for selection.
// pickupLocations?: Array<{ id: string, name: string, lat: number | null, lng: number | null }>;
locationLat?: number | null; locationLat?: number | null;
locationLng?: number | null; locationLng?: number | null;
timezone?: string; timezone?: string;
@ -500,6 +502,10 @@ class DateTimeWidget {
return; return;
} }
// TODO: When backend supports `pickupLocations`, implement a UI selector (list/map)
// to let the user choose a location before rendering dates. Currently, it just defaults
// to the single returned location.
if (method.value === "PICKUP" && this.config.googleMapsApiKey) { if (method.value === "PICKUP" && this.config.googleMapsApiKey) {
this.showPickupMap(); this.showPickupMap();
} }